-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Avoid enumeration allocation via interface #4505
Comments
It does: the same Enumerator is returned. But when accessed via the interface, you're using the interface method that's typed to return |
Wouldn't it be |
Ah, that's non-generic |
No, it has a strongly-typed struct Enumerator that's returned from its GetEnumerator() method: foreach binds to this pattern more tightly than it does to the interface implementation. |
Obviously is been this way for years; but could there any potential work around similar to the one for arrays? var array = new string[0];
var iList = (IList<string>)array;
for (var i = 0; i < 1000000; i++)
{
// Doesn't allocate
foreach (var item in array)
{
;
}
}
for (var i = 0; i < 1000000; i++)
{
// Doesn't allocate
foreach (var item in iList)
{
;
}
} Doesn't alloc obv in picture (comment is wrong) |
Are you specifically concerned about the case of iterating over an empty Dictionary via the interface? That's the only one that array optimizes in this way. |
Ah yeah :( For reference: Was wondering if the was some kind of pattern that could be introduced, for example if the interface enumerator returned the result of the strongly typed enumerator function: IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() {
return GetEnumerator();
} The jit could resolve down one level; and bind to the strongly typed enumerator? |
It could, in certain cases. This is related to devirtualization, see #4489 as well. That said, if you want performance it's best to avoid interfaces. Code like the above may be optimized but such code doesn't make sense and similar code that does make sense is less likely to be optimized. |
Would be happy if it worked with the standard generic collections and their generic interface's IEnumerables/foreach as a starter; obviously it would be better if their was a pattern that could be used for people who implemented those interfaces. |
The best that collection implementers can do is to ensure that the explicitly implemented GetEnumerator returns the a struct enumerator just like the "ducking" GetEnumerator does, the standard generic collections already do that. Beyond that it's up to the JIT compiler to figure it out. |
For context; in aspnet iterating the headers IDictionary in aspnet allocates 256 kBytes for request and 256 kBytes for response per 4k requests (bottom line): When used as a Dictionary it doesn't allocate anything (bottom line): However the default implementation of the headers IDictionary in Kestrel isn't a Dictionary; and it can also be user overridden, hence the importance of the interface, so it can't be cast or expressed as concrete type. This flexibility comes at a cost for 1M rps of 128 MBytes of garbage per second; and at 6M rps of 768 MBytes per second or 46 GBytes per minute for the GC to clean up by using an interface for Enumerable. |
I know that the extra allocations are the main issue, but just for reference here's the perf difference: BenchmarkDotNet=v0.7.7.0
OS=Microsoft Windows NT 6.1.7601 Service Pack 1
Processor=Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz, ProcessorCount=8
HostCLR=MS.NET 4.0.30319.0, Arch=64-bit [RyuJIT]
Type=Framework_DictionaryVsIDictionary Mode=Throughput Platform=HostPlatform Jit=HostJit .NET=HostFramework
and here's the BenchmarkDotNet benchmark I used (easier than hand-rolling benchmarks each time) public class Framework_DictionaryVsIDictionary
{
Dictionary<string, string> dict;
IDictionary<string, string> idict;
[Setup]
public void Setup()
{
dict = new Dictionary<string, string>();
idict = (IDictionary<string, string>)dict;
}
[Benchmark]
public Dictionary<string, string> DictionaryEnumeration()
{
// Doesn't allocate
foreach (var item in dict)
{
;
}
return dict;
}
[Benchmark]
public IDictionary<string, string> IDictionaryEnumeration()
{
// Allocates 998k
foreach (var item in idict)
{
;
}
return idict;
}
} |
And just for completeness, here's the results when using different sized
The benchmark now looks like this: public class Framework_DictionaryVsIDictionary
{
Dictionary<string, string> dict;
IDictionary<string, string> idict;
[Params(0, 1, 5, 10, 100, 1000)]
public int InitialSize = 0;
[Setup]
public void Setup()
{
dict = new Dictionary<string, string>(InitialSize);
idict = (IDictionary<string, string>)dict;
foreach (var value in Enumerable.Range(0, InitialSize))
dict.Add(value.ToString(), "Value:" + value.ToString());
}
[Benchmark]
public Dictionary<string, string> DictionaryEnumeration()
{
// Doesn't allocate
foreach (var item in dict)
{
;
}
return dict;
}
[Benchmark]
public IDictionary<string, string> IDictionaryEnumeration()
{
// Allocates 998k
foreach (var item in idict)
{
;
}
return idict;
}
} |
Yes, the issue isn't just enumerator allocations, it's also interface-based dispatch. In addition to boxing the enumerator, the MoveNext and Current calls made per element go from being potentially-inlineable non-virtual calls to being interface calls. |
ah, thanks for the explanation, that explains the "extra" overhead I see when doing the benchmarks with the different |
I don't think there's any action to be taken here. If there's a concrete request for something specific to be done, please feel free to reopen and share. Thanks. |
Can the Jitter/runtime resolve down one level when calling
foreach
on an interface where a strongly typed enumerator exists? Or otherwise prevent boxing; can always just be for theforeach
case.There may need to be a pattern; for example the Interface enumerator returning result of call to strongly typed Enumerator.
Enumerating
Dictionary
doesn't allocate; however casting to anIDictionary
then enumerating allocates. Shouldn't it follow the same code path asDictionary
?The text was updated successfully, but these errors were encountered: