Closed redknightlois closed 3 years ago
Would be interesting to see NGEN results too. It seemed to be on the conservative side. Does it even use the 'current' JIT? Can you switch it? If so, I can test a lot more code for it ;p
You will have to ask @AndreyAkinshin if he supports NGEN on BenchmarkDotNet. :)
@leppie NGEN doesn't use a JIT at all, pretty much by definition.
The purpose of a JIT is to build quick-and-dirty executable code ASAP, at the expense of quality. An AOT compiler takes its time to produce a higher-quality build.
@masonwheeler I understand that, but they presumably support a large common codebase.
[snip: corrected] * thought NGEN was native code generation.
@leppie @masonwheeler @Eyas NGEN (aka crossgen in CoreCLR) uses the same compiler, RyuJIT in this case. In general there are no differences between code that's generated at runtime and code that's generated by NGEN.
@redknightlois I took a quick look at the hash case and it may be that the difference in performance is caused by an optimization related to the buffer
variable - those separate buffer += sizeof(uint)
get folded into a single addition and into the memory operands generated for pointer dereferences.
Changing code like below (hope I didn't break it :sleeping:) results in better performance with RyuJIT while keeping the same performance with JIT64:
uint* bfi = (uint*)buffer;
do {
v1 += bfi[0] * PRIME32_2;
v2 += bfi[1] * PRIME32_2;
v3 += bfi[2] * PRIME32_2;
v4 += bfi[3] * PRIME32_2;
bfi += 4;
v1 = RotateLeft32(v1, 13);
v2 = RotateLeft32(v2, 13);
v3 = RotateLeft32(v3, 13);
v4 = RotateLeft32(v4, 13);
v1 *= PRIME32_1;
v2 *= PRIME32_1;
v3 *= PRIME32_1;
v4 *= PRIME32_1;
}
while (bfi <= limit);
@mikedn That should work, originally I havent use indexing because JIT64 is a bit unpredictable on when it hits an optimized path and when it doesnt. So I learned to avoid that pattern altogether.
@leppie, @redknightlois, BenchmarkDotNet doesn't support NGEN. However,
It looks like the issue with the CopySmall, CopyVerySmall and CopyTiny cases is that CopyInline
is not inlined by RyuJIT but it is inlined by JIT64.
The compare cases appear to be affected by loop head/branch target alignment. There's also a potential issue in the tail loop that contains 2 redundant loads which RyuJIT does not eliminate. It could be changed to:
TAIL:
while (last > 0)
{
byte x = *bpx;
byte y = *pby;
if (x != y)
return x - y;
bpx++;
bpy++;
last--;
}
Its definitely an improvement, but with 16MB buffers still the difference is noticeable.
// BenchmarkDotNet=v0.7.6.0 // OS=Microsoft Windows NT 6.2.9200.0 // Processor=Intel(R) Core(TM) i5-3470 CPU @ 3.20GHz, ProcessorCount=4 // CLR=MS.NET 4.0.30319.42000, Arch=32-bit Common: Type=HashingBenchmark Mode=Throughput Platform=X64 .NET=Current
Method | Jit | AvrTime | StdDev | op/s |
---|---|---|---|---|
HashBlock | LegacyJit | 110.98 ns | 1.57 ns | 9010673.07 |
HashBlock | RyuJit | 127.91 ns | 0.948 ns | 7818076.43 |
HashBlockAlt | LegacyJit | 108.08 ns | 2.88 ns | 9252102.73 |
HashBlockAlt | RyuJit | 110.14 ns | 0.754 ns | 9079637.52 |
HashHuge | LegacyJit | 3.46 ms | 17.55 us | 288.93 |
HashHuge | RyuJit | 4.01 ms | 21.41 us | 249.09 |
HashHugeAlt | LegacyJit | 3.51 ms | 26.32 us | 284.6 |
HashHugeAlt | RyuJit | 3.78 ms | 341.05 us | 264.66 |
Given that the benchmark code appears to no longer be available, and there's nothing obviously actionable here, I'm going to go ahead and close this.
TLDR: With the exception of a single scenario where calling native (which I am very glad it was optimized) for all the rest RyuJIT is slower (sometimes by a big margin).
Since I discovered https://github.com/dotnet/coreclr/issues/1306 I have been testing some of the most performance sensitives operations we have. I tested 3 different optimized routines, unsafe optimized memory copy, unsafe memory compare and unsafe xxHash32 hashing algorithm.
The code for all of those is available on github.
Sizes are the following:
See benchmark code for details: https://gist.github.com/redknightlois/f71700d2705a7f9fe312
Hashing with xxHash32
// BenchmarkDotNet=v0.7.6.0 // OS=Microsoft Windows NT 6.2.9200.0 // Processor=Intel(R) Core(TM) i5-3470 CPU @ 3.20GHz, ProcessorCount=4 // CLR=MS.NET 4.0.30319.42000, Arch=32-bit Common: Type=HashingBenchmark Mode=Throughput Platform=X64 .NET=Current
The difference in throughput for 16Mb buffers is 4.61 Gigabytes per second from the LegacyJit to 3.99 Gigabytes per sec with RyuJIT. (600 Mb per second difference which is not a minimal difference).
Memory Compare
// BenchmarkDotNet=v0.7.6.0 // OS=Microsoft Windows NT 6.2.9200.0 // Processor=Intel(R) Core(TM) i5-3470 CPU @ 3.20GHz, ProcessorCount=4 // CLR=MS.NET 4.0.30319.42000, Arch=32-bit Common: Type=MemoryCompareBenchmark Mode=Throughput Platform=X64 .NET=Current
Again in the case of memory compare the difference still exist, but it is not that big. 5.24Gb/sec for RyuJIT vs. 5.48Gb/sec for the LegacyJIT.
Memory Copy
// BenchmarkDotNet=v0.7.6.0 // OS=Microsoft Windows NT 6.2.9200.0 // Processor=Intel(R) Core(TM) i5-3470 CPU @ 3.20GHz, ProcessorCount=4 // CLR=MS.NET 4.0.30319.42000, Arch=32-bit Common: Type=MemoryCopyBenchmark Mode=Throughput Platform=X64 .NET=Current
It appears that for very small RyuJit is worse (16 bytes), for middle size (64 bytes), and for big LegacyJIT is worse ( 4096+ bytes )
For buffers of size 16 MB the throughput is 7.18Gb/sec for RyuJIT to 6.4Gb/sec for the LegacyJIT; but we lost for the small buffers. This means that the marshaling of calls to unmanaged code has been greatly improved. (kudos on that)
category:cq theme:loop-opt skill-level:intermediate cost:medium