Aquí me estoy golpeando la cabeza contra la pared, así que espero que algunos de ustedes puedan educarme. Estaba haciendo algunos puntos de referencia de rendimiento usando BenchmarkDotNet y me encontré con este extraño caso en el que parece que declarar a un miembro const
degrada considerablemente el rendimiento.
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
namespace PerfTest
{
[DisassemblyDiagnoser(printAsm: true, printSource: true)]
public class Test
{
private int[] data;
private int Threshold = 90;
private const int ConstThreshold = 90;
[GlobalSetup]
public void GlobalSetup()
{
data = new int[1000];
var random = new Random(42);
for (var i = 0; i < data.Length; i++)
{
data[i] = random.Next(100);
}
}
static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<Test>();
}
[Benchmark(Baseline = true)]
public void ClampToMemberValue()
{
for (var i = 0; i < data.Length; i++)
{
if (data[i] > Threshold) data[i] = Threshold;
}
}
[Benchmark]
public void ClampToConstValue()
{
for (var i = 0; i < data.Length; i++)
{
if (data[i] > ConstThreshold) data[i] = ConstThreshold;
}
}
}
}
Tenga en cuenta que la única diferencia entre los dos métodos de prueba es si se comparan con una variable miembro regular o un miembro constante.
Según BenchmarkDotNet, el uso del valor const es significativamente más lento y no entiendo por qué.
BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362
Intel Core i7-5820K CPU 3.30GHz (Broadwell), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.0.100
[Host] : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64bit RyuJIT
DefaultJob : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64bit RyuJIT
| Method | Mean | Error | StdDev | Ratio |
|------------------- |---------:|---------:|---------:|------:|
| ClampToMemberValue | 590.4 ns | 1.980 ns | 1.852 ns | 1.00 |
| ClampToConstValue | 724.6 ns | 4.184 ns | 3.709 ns | 1.23 |
Mirar el código compilado JIT no lo explica por lo que puedo decir. Aquí está el código para los dos métodos. La única diferencia es si la comparación se realiza contra un registro o un literal.
00007ff9`7f1b8500 PerfTest.Test.ClampToMemberValue()
for (var i = 0; i < data.Length; i++)
^^^^^^^^^
00007ff9`7f1b8504 33c0 xor eax,eax
for (var i = 0; i < data.Length; i++)
^^^^^^^^^^^^^^^
00007ff9`7f1b8506 488b5108 mov rdx,qword ptr [rcx+8]
00007ff9`7f1b850a 837a0800 cmp dword ptr [rdx+8],0
00007ff9`7f1b850e 7e2e jle 00007ff9`7f1b853e
00007ff9`7f1b8510 8b4910 mov ecx,dword ptr [rcx+10h]
if (data[i] > Threshold) data[i] = Threshold;
^^^^^^^^^^^^^^^^^^^^^^^^
00007ff9`7f1b8513 4c8bc2 mov r8,rdx
00007ff9`7f1b8516 458b4808 mov r9d,dword ptr [r8+8]
00007ff9`7f1b851a 413bc1 cmp eax,r9d
00007ff9`7f1b851d 7324 jae 00007ff9`7f1b8543
00007ff9`7f1b851f 4c63c8 movsxd r9,eax
00007ff9`7f1b8522 43394c8810 cmp dword ptr [r8+r9*4+10h],ecx
00007ff9`7f1b8527 7e0e jle 00007ff9`7f1b8537
if (data[i] > Threshold) data[i] = Threshold;
^^^^^^^^^^^^^^^^^^^^
00007ff9`7f1b8529 4c8bc2 mov r8,rdx
00007ff9`7f1b852c 448bc9 mov r9d,ecx
00007ff9`7f1b852f 4c63d0 movsxd r10,eax
00007ff9`7f1b8532 47894c9010 mov dword ptr [r8+r10*4+10h],r9d
for (var i = 0; i < data.Length; i++)
^^^
00007ff9`7f1b8537 ffc0 inc eax
00007ff9`7f1b8539 394208 cmp dword ptr [rdx+8],eax
00007ff9`7f1b853c 7fd5 jg 00007ff9`7f1b8513
}
^
00007ff9`7f1b853e 4883c428 add rsp,28h
y
00007ff9`7f1a8500 PerfTest.Test.ClampToConstValue()
for (var i = 0; i < data.Length; i++)
^^^^^^^^^
00007ff9`7f1a8504 33c0 xor eax,eax
for (var i = 0; i < data.Length; i++)
^^^^^^^^^^^^^^^
00007ff9`7f1a8506 488b5108 mov rdx,qword ptr [rcx+8]
00007ff9`7f1a850a 837a0800 cmp dword ptr [rdx+8],0
00007ff9`7f1a850e 7e2d jle 00007ff9`7f1a853d
if (data[i] > ConstThreshold) data[i] = ConstThreshold;
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
00007ff9`7f1a8510 488bca mov rcx,rdx
00007ff9`7f1a8513 448b4108 mov r8d,dword ptr [rcx+8]
00007ff9`7f1a8517 413bc0 cmp eax,r8d
00007ff9`7f1a851a 7326 jae 00007ff9`7f1a8542
00007ff9`7f1a851c 4c63c0 movsxd r8,eax
00007ff9`7f1a851f 42837c81105a cmp dword ptr [rcx+r8*4+10h],5Ah
00007ff9`7f1a8525 7e0f jle 00007ff9`7f1a8536
if (data[i] > ConstThreshold) data[i] = ConstThreshold;
^^^^^^^^^^^^^^^^^^^^^^^^^
00007ff9`7f1a8527 488bca mov rcx,rdx
00007ff9`7f1a852a 4c63c0 movsxd r8,eax
00007ff9`7f1a852d 42c74481105a000000 mov dword ptr [rcx+r8*4+10h],5Ah
for (var i = 0; i < data.Length; i++)
^^^
00007ff9`7f1a8536 ffc0 inc eax
00007ff9`7f1a8538 394208 cmp dword ptr [rdx+8],eax
00007ff9`7f1a853b 7fd3 jg 00007ff9`7f1a8510
}
^
00007ff9`7f1a853d 4883c428 add rsp,28h
Estoy seguro de que hay algo que he pasado por alto, pero no puedo entenderlo en este punto, así que estoy buscando información sobre lo que puede explicar esto.
fuente
cmp
ymov
que se usan para la ruta constante ocupan más memoria que las instrucciones basadas en un registro porque la codificación de un número requiere bytes adicionales y, en total, requieren más ciclos de CPU para ejecutarse (9 bytes vs 5 bytes paramov
y 6 bytes vs 5 bytes para cmp) . Y a pesar de que haymov ecx,dword ptr [rcx+10h]
instrucciones adicionales para la versión no constante, es muy probable que el compilador JIT esté optimizado para estar fuera del bucle en la versión de lanzamiento.Respuestas:
Mirando https://benchmarkdotnet.org/articles/features/setup-and-cleanup.html
Creo que deberías estar usando en
[IterationSetup]
lugar de[GlobalSetup]
. Con la configuración global,data
se cambia una vez y luegodata
se reutiliza en los puntos de referencia.Entonces, he cambiado el código para usar la inicialización adecuada. Variables modificadas para que los controles sean más frecuentes. Y agregó algunas variaciones más.
Los resultados parecen más normales:
No parece haber ninguna diferencia entre las diferentes variantes. O bien todo está optimizado o const int no está optimizado de ninguna manera en este escenario.
fuente
GlobalSetup
se ejecuta dos veces, una vez antes de cada Benchmark, por lo que ambos métodos comienzan con la misma condición previa.