visual tipos tablas studio sirve referencia que programacion para lenguaje flotantes flotante float declaracion datos c# performance memory-management microbenchmark

c# - tablas - ¿Por qué es más lento sumar un conjunto de tipos de valor que un conjunto de tipos de referencia?



tipos de datos en c# pdf (4)

Estoy tratando de entender mejor cómo funciona la memoria en .NET, así que estoy jugando con BenchmarkDotNet y los diagnozers . He creado una prueba comparativa que compara el rendimiento de la class y la struct al sumar elementos de matriz. Esperaba que la suma de los tipos de valor siempre fuera más rápida. Pero para matrices cortas no lo es. ¿Alguien puede explicar eso?

El código:

internal class ReferenceType { public int Value; } internal struct ValueType { public int Value; } internal struct ExtendedValueType { public int Value; private double _otherData; // this field is here just to make the object bigger }

Tengo tres matrices:

private ReferenceType[] _referenceTypeData; private ValueType[] _valueTypeData; private ExtendedValueType[] _extendedValueTypeData;

Que inicializo con el mismo conjunto de valores aleatorios.

A continuación, un método de referencia:

[Benchmark] public int ReferenceTypeSum() { var sum = 0; for (var i = 0; i < Size; i++) { sum += _referenceTypeData[i].Value; } return sum; }

Size es un parámetro de referencia. Otros dos métodos de referencia ( ValueTypeSum y ExtendedValueTypeSum ) son idénticos, a excepción de que estoy sumando en _valueTypeData o _extendedValueTypeData . Código completo para el benchmark .

Resultados de referencia:

DefaultJob: .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64 bits RyuJIT-v4.7.3190.0

Method | Size | Mean | Error | StdDev | Ratio | RatioSD | --------------------- |----- |----------:|----------:|----------:|------:|--------:| ReferenceTypeSum | 100 | 75.76 ns | 1.2682 ns | 1.1863 ns | 1.00 | 0.00 | ValueTypeSum | 100 | 79.83 ns | 0.3866 ns | 0.3616 ns | 1.05 | 0.02 | ExtendedValueTypeSum | 100 | 78.70 ns | 0.8791 ns | 0.8223 ns | 1.04 | 0.01 | | | | | | | | ReferenceTypeSum | 500 | 354.78 ns | 3.9368 ns | 3.6825 ns | 1.00 | 0.00 | ValueTypeSum | 500 | 367.08 ns | 5.2446 ns | 4.9058 ns | 1.03 | 0.01 | ExtendedValueTypeSum | 500 | 346.18 ns | 2.1114 ns | 1.9750 ns | 0.98 | 0.01 | | | | | | | | ReferenceTypeSum | 1000 | 697.81 ns | 6.8859 ns | 6.1042 ns | 1.00 | 0.00 | ValueTypeSum | 1000 | 720.64 ns | 5.5592 ns | 5.2001 ns | 1.03 | 0.01 | ExtendedValueTypeSum | 1000 | 699.12 ns | 9.6796 ns | 9.0543 ns | 1.00 | 0.02 |

Core: .NET Core 2.1.4 (CoreCLR 4.6.26814.03, CoreFX 4.6.26814.02), 64bit RyuJIT

Method | Size | Mean | Error | StdDev | Ratio | RatioSD | --------------------- |----- |----------:|----------:|----------:|------:|--------:| ReferenceTypeSum | 100 | 76.22 ns | 0.5232 ns | 0.4894 ns | 1.00 | 0.00 | ValueTypeSum | 100 | 80.69 ns | 0.9277 ns | 0.8678 ns | 1.06 | 0.01 | ExtendedValueTypeSum | 100 | 78.88 ns | 1.5693 ns | 1.4679 ns | 1.03 | 0.02 | | | | | | | | ReferenceTypeSum | 500 | 354.30 ns | 2.8682 ns | 2.5426 ns | 1.00 | 0.00 | ValueTypeSum | 500 | 372.72 ns | 4.2829 ns | 4.0063 ns | 1.05 | 0.01 | ExtendedValueTypeSum | 500 | 357.50 ns | 7.0070 ns | 6.5543 ns | 1.01 | 0.02 | | | | | | | | ReferenceTypeSum | 1000 | 696.75 ns | 4.7454 ns | 4.4388 ns | 1.00 | 0.00 | ValueTypeSum | 1000 | 697.95 ns | 2.2462 ns | 2.1011 ns | 1.00 | 0.01 | ExtendedValueTypeSum | 1000 | 687.75 ns | 2.3861 ns | 1.9925 ns | 0.99 | 0.01 |

He ejecutado la BranchMispredictions comparativa con los contadores de hardware de BranchMispredictions y CacheMisses , pero no hay errores de caché ni predicciones erróneas de sucursales. También he comprobado el código IL de la versión, y los métodos de referencia difieren solo por las instrucciones que cargan las variables de tipo de valor o referencia.

Para tamaños de arreglos más grandes, sumar el tipo de valor es siempre más rápido (por ejemplo, porque los tipos de valor ocupan menos memoria), pero no entiendo por qué es más lento para los arreglos más cortos. ¿Qué extraño aquí? ¿Y por qué hacer la struct más grande (ver ExtendedValueType ) hace que sumar un poco más rápido?

---- ACTUALIZACIÓN ----

Inspirado por un comentario hecho por @usr, he vuelto a ejecutar el punto de referencia con LegacyJit. También he agregado el diagnóstico de memoria inspirado en @Silver Shroud (sí, no hay asignaciones de almacenamiento dinámico).

Job = LegacyJitX64 Jit = LegacyJit Platform = X64 Runtime = Clr

Method | Size | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------------- |----- |-----------:|-----------:|-----------:|------:|--------:|------------:|------------:|------------:|--------------------:| ReferenceTypeSum | 100 | 110.1 ns | 0.6836 ns | 0.6060 ns | 1.00 | 0.00 | - | - | - | - | ValueTypeSum | 100 | 109.5 ns | 0.4320 ns | 0.4041 ns | 0.99 | 0.00 | - | - | - | - | ExtendedValueTypeSum | 100 | 109.5 ns | 0.5438 ns | 0.4820 ns | 0.99 | 0.00 | - | - | - | - | | | | | | | | | | | | ReferenceTypeSum | 500 | 517.8 ns | 10.1271 ns | 10.8359 ns | 1.00 | 0.00 | - | - | - | - | ValueTypeSum | 500 | 511.9 ns | 7.8204 ns | 7.3152 ns | 0.99 | 0.03 | - | - | - | - | ExtendedValueTypeSum | 500 | 534.7 ns | 3.0168 ns | 2.8219 ns | 1.03 | 0.02 | - | - | - | - | | | | | | | | | | | | ReferenceTypeSum | 1000 | 1,058.3 ns | 8.8829 ns | 8.3091 ns | 1.00 | 0.00 | - | - | - | - | ValueTypeSum | 1000 | 1,048.4 ns | 8.6803 ns | 8.1196 ns | 0.99 | 0.01 | - | - | - | - | ExtendedValueTypeSum | 1000 | 1,057.5 ns | 5.9456 ns | 5.5615 ns | 1.00 | 0.01 | - | - | - | - |

Los resultados JIT heredados son los esperados, pero más lentos que los resultados anteriores. Lo que sugiere que RyuJit hace algunas mejoras de rendimiento mágico, que funcionan mejor en los tipos de referencia.

---- ACTUALIZACIÓN 2 ----

Gracias por las grandes respuestas! ¡He aprendido mucho!

Por debajo de los resultados de otro punto de referencia. Estoy comparando los métodos originalmente evaluados, métodos optimizados, según lo sugerido por @usr y @xoofx:

[Benchmark] public int ReferenceTypeOptimizedSum() { var sum = 0; var array = _referenceTypeData; for (var i = 0; i < array.Length; i++) { sum += array[i].Value; } return sum; }

y versiones desenrolladas, según lo sugerido por @AndreyAkinshin, con las optimizaciones anteriores agregadas:

[Benchmark] public int ReferenceTypeUnrolledSum() { var sum = 0; var array = _referenceTypeData; for (var i = 0; i < array.Length; i += 16) { sum += array[i].Value; sum += array[i + 1].Value; sum += array[i + 2].Value; sum += array[i + 3].Value; sum += array[i + 4].Value; sum += array[i + 5].Value; sum += array[i + 6].Value; sum += array[i + 7].Value; sum += array[i + 8].Value; sum += array[i + 9].Value; sum += array[i + 10].Value; sum += array[i + 11].Value; sum += array[i + 12].Value; sum += array[i + 13].Value; sum += array[i + 14].Value; sum += array[i + 15].Value; } return sum; }

Código completo aquí.

Resultados de referencia:

BenchmarkDotNet = v0.11.3, OS = Windows 10.0.17134.345 (1803 / April2018Update / Redstone4) Intel Core i5-6400 CPU 2.70GHz (Skylake), 1 CPU, 4 lógicos y 4 núcleos físicos Frecuencia = 2648439 Hz, Resolución = 377.5809 ns, Temporizador = TSC

DefaultJob: .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64 bits RyuJIT-v4.7.3190.0

Method | Size | Mean | Error | StdDev | Ratio | RatioSD | ------------------------------ |----- |---------:|----------:|----------:|------:|--------:| ReferenceTypeSum | 512 | 344.8 ns | 3.6473 ns | 3.4117 ns | 1.00 | 0.00 | ValueTypeSum | 512 | 361.2 ns | 3.8004 ns | 3.3690 ns | 1.05 | 0.02 | ExtendedValueTypeSum | 512 | 347.2 ns | 5.9686 ns | 5.5831 ns | 1.01 | 0.02 | ReferenceTypeOptimizedSum | 512 | 254.5 ns | 2.4427 ns | 2.2849 ns | 0.74 | 0.01 | ValueTypeOptimizedSum | 512 | 353.0 ns | 1.9201 ns | 1.7960 ns | 1.02 | 0.01 | ExtendedValueTypeOptimizedSum | 512 | 280.3 ns | 1.2423 ns | 1.0374 ns | 0.81 | 0.01 | ReferenceTypeUnrolledSum | 512 | 213.2 ns | 1.2483 ns | 1.1676 ns | 0.62 | 0.01 | ValueTypeUnrolledSum | 512 | 201.3 ns | 0.6720 ns | 0.6286 ns | 0.58 | 0.01 | ExtendedValueTypeUnrolledSum | 512 | 223.6 ns | 1.0210 ns | 0.9550 ns | 0.65 | 0.01 |


Creo que la razón para que el resultado sea tan cercano es usar un tamaño que sea tan pequeño y no asignar nada en el montón (dentro de su ciclo de inicialización de matriz) para fragmentar elementos de matriz de objeto.

En su código de referencia, solo los elementos de la matriz de objetos se asignan desde el montón (*), de esta manera MemoryAllocator puede asignar cada elemento secuencialmente (**) en el montón. Cuando el código de referencia comienza a ejecutarse, los datos se leerán de las memorias caché de la CPU a la CPU y, dado que los elementos de la matriz de objetos se escribirán en la secuencia en un orden secuencial (en un bloque contiguo), se almacenarán en la memoria caché y es por eso que no obtiene ningún error de memoria caché.

Para ver esto mejor, puede tener otra matriz de objetos (preferiblemente con objetos más grandes) que se asignará en el montón para fragmentar sus elementos de matriz de objetos de referencia. Esto puede hacer que las fallas de caché ocurran antes de su configuración actual. En un escenario de la vida real, habrá otros subprocesos que se asignarán en el mismo montón y fragmentarán aún más los objetos reales de la matriz. También acceder a la RAM lleva mucho más tiempo que acceder a la caché de la CPU (o un ciclo de la CPU). (Marque esta post respecto a este tema).

(*) La matriz ValueType asigna todo el espacio requerido para los elementos de la matriz cuando lo inicializa con el new ValueType[Size] ; Los elementos de la matriz ValueType serán contiguos en el ram.

(**) objectArr [i] object element y objectArr [i + 1] (y así sucesivamente) estarán lado a lado en el montón, cuando el bloque ram se almacene en caché, probablemente todos los elementos de la matriz de objetos se leerán en el caché de CPU, por lo que no se requerirá acceso a ram cuando se itera sobre la matriz.


En Haswell, Intel introdujo estrategias adicionales para la predicción de bifurcaciones para pequeños bucles (por eso no podemos observar esta situación en IvyBridge). Parece que una estrategia de rama particular depende de muchos factores, incluida la alineación del código nativo. La diferencia entre LegacyJIT y RyuJIT puede explicarse por diferentes estrategias de alineación para los métodos. Desafortunadamente, no puedo proporcionar todos los detalles relevantes de este fenómeno de rendimiento (Intel mantiene los detalles de la implementación en secreto; mis conclusiones se basan solo en mis propios experimentos de ingeniería inversa de CPU), pero puedo decirle cómo mejorar este punto de referencia.

El principal truco que mejora sus resultados es el desenrollado manual de bucles, que es fundamental para las nanobenchmarks en Haswell + con RyuJIT. Los fenómenos anteriores solo afectan a los bucles pequeños, por lo que podemos resolver el problema con un cuerpo de bucle enorme. De hecho, cuando tienes un punto de referencia como

[Benchmark] public void MyBenchmark() { Foo(); }

BenchmarkDotNet genera el siguiente bucle:

for (int i = 0; i < N; i++) { Foo(); Foo(); Foo(); Foo(); Foo(); Foo(); Foo(); Foo(); Foo(); Foo(); Foo(); Foo(); Foo(); Foo(); Foo(); Foo(); }

Puede controlar el número de invocaciones internas en este bucle a través de UnrollFactor . Si tiene un pequeño bucle dentro de un punto de referencia, debe desenrollarlo de la misma manera:

[Benchmark(Baseline = true)] public int ReferenceTypeSum() { var sum = 0; for (var i = 0; i < Size; i += 16) { sum += _referenceTypeData[i].Value; sum += _referenceTypeData[i + 1].Value; sum += _referenceTypeData[i + 2].Value; sum += _referenceTypeData[i + 3].Value; sum += _referenceTypeData[i + 4].Value; sum += _referenceTypeData[i + 5].Value; sum += _referenceTypeData[i + 6].Value; sum += _referenceTypeData[i + 7].Value; sum += _referenceTypeData[i + 8].Value; sum += _referenceTypeData[i + 9].Value; sum += _referenceTypeData[i + 10].Value; sum += _referenceTypeData[i + 11].Value; sum += _referenceTypeData[i + 12].Value; sum += _referenceTypeData[i + 13].Value; sum += _referenceTypeData[i + 14].Value; sum += _referenceTypeData[i + 15].Value; } return sum; }

Otro truco es el calentamiento agresivo (por ejemplo, 30 iteraciones). Así es como se ve la etapa de calentamiento en mi máquina:

WorkloadWarmup 1: 4194304 op, 865744000.00 ns, 206.4095 ns/op WorkloadWarmup 2: 4194304 op, 892164000.00 ns, 212.7085 ns/op WorkloadWarmup 3: 4194304 op, 861913000.00 ns, 205.4961 ns/op WorkloadWarmup 4: 4194304 op, 868044000.00 ns, 206.9578 ns/op WorkloadWarmup 5: 4194304 op, 933894000.00 ns, 222.6577 ns/op WorkloadWarmup 6: 4194304 op, 890567000.00 ns, 212.3277 ns/op WorkloadWarmup 7: 4194304 op, 923509000.00 ns, 220.1817 ns/op WorkloadWarmup 8: 4194304 op, 861953000.00 ns, 205.5056 ns/op WorkloadWarmup 9: 4194304 op, 862454000.00 ns, 205.6251 ns/op WorkloadWarmup 10: 4194304 op, 862565000.00 ns, 205.6515 ns/op WorkloadWarmup 11: 4194304 op, 867301000.00 ns, 206.7807 ns/op WorkloadWarmup 12: 4194304 op, 841892000.00 ns, 200.7227 ns/op WorkloadWarmup 13: 4194304 op, 827717000.00 ns, 197.3431 ns/op WorkloadWarmup 14: 4194304 op, 828257000.00 ns, 197.4719 ns/op WorkloadWarmup 15: 4194304 op, 812275000.00 ns, 193.6615 ns/op WorkloadWarmup 16: 4194304 op, 792011000.00 ns, 188.8301 ns/op WorkloadWarmup 17: 4194304 op, 792607000.00 ns, 188.9722 ns/op WorkloadWarmup 18: 4194304 op, 794428000.00 ns, 189.4064 ns/op WorkloadWarmup 19: 4194304 op, 794879000.00 ns, 189.5139 ns/op WorkloadWarmup 20: 4194304 op, 794914000.00 ns, 189.5223 ns/op WorkloadWarmup 21: 4194304 op, 794061000.00 ns, 189.3189 ns/op WorkloadWarmup 22: 4194304 op, 793385000.00 ns, 189.1577 ns/op WorkloadWarmup 23: 4194304 op, 793851000.00 ns, 189.2688 ns/op WorkloadWarmup 24: 4194304 op, 793456000.00 ns, 189.1747 ns/op WorkloadWarmup 25: 4194304 op, 794194000.00 ns, 189.3506 ns/op WorkloadWarmup 26: 4194304 op, 793980000.00 ns, 189.2996 ns/op WorkloadWarmup 27: 4194304 op, 804402000.00 ns, 191.7844 ns/op WorkloadWarmup 28: 4194304 op, 801002000.00 ns, 190.9738 ns/op WorkloadWarmup 29: 4194304 op, 797860000.00 ns, 190.2246 ns/op WorkloadWarmup 30: 4194304 op, 802668000.00 ns, 191.3710 ns/op

De forma predeterminada, BenchmarkDotNet intenta detectar tales situaciones y aumentar el número de iteraciones de calentamiento. Desafortunadamente, no siempre es posible (suponiendo que queremos tener una etapa de calentamiento "rápida" en casos "simples").

Y aquí están mis resultados (puede encontrar la lista completa del índice de referencia actualizado aquí: https://gist.github.com/AndreyAkinshin/4c9e0193912c99c0b314359d5c5d0a4e ):

BenchmarkDotNet=v0.11.3, OS=macOS Mojave 10.14.1 (18B75) [Darwin 18.2.0] Intel Core i7-4870HQ CPU 2.50GHz (Haswell), 1 CPU, 8 logical and 4 physical cores .NET Core SDK=3.0.100-preview-009812 [Host] : .NET Core 2.0.5 (CoreCLR 4.6.0.0, CoreFX 4.6.26018.01), 64bit RyuJIT Job-IHBGGW : .NET Core 2.0.5 (CoreCLR 4.6.0.0, CoreFX 4.6.26018.01), 64bit RyuJIT IterationCount=30 WarmupCount=30 Method | Size | Mean | Error | StdDev | Median | Ratio | RatioSD | --------------------- |----- |---------:|----------:|----------:|---------:|------:|--------:| ReferenceTypeSum | 256 | 180.7 ns | 0.4514 ns | 0.6474 ns | 180.8 ns | 1.00 | 0.00 | ValueTypeSum | 256 | 154.4 ns | 1.8844 ns | 2.8205 ns | 153.3 ns | 0.86 | 0.02 | ExtendedValueTypeSum | 256 | 183.1 ns | 2.2283 ns | 3.3352 ns | 181.1 ns | 1.01 | 0.02 |


Este es de hecho un comportamiento muy extraño.

El código generado para el bucle central para el tipo de referencia es así:

M00_L00: mov r9,rcx cmp edx,[r9+8] jae ArrayOutOfBound movsxd r10,edx mov r9,[r9+r10*8+10h] add eax,[r9+8] inc edx cmp edx,r8d jl M00_L00

mientras que para el bucle de tipo de valor:

M00_L00: mov r9,rcx cmp edx,[r9+8] jae ArrayOutOfBound movsxd r10,edx add eax,[r9+r10*4+10h] inc edx cmp edx,r8d jl M00_L00

Así que la diferencia se reduce a:

Para el tipo de referencia :

mov r9,[r9+r10*8+10h] add eax,[r9+8]

Para el tipo de valor :

add eax,[r9+r10*4+10h]

Con una instrucción y sin acceso indirecto a la memoria, el tipo de valor debería ser más rápido ...

Intenté ejecutar esto a través de Intel Architecture Code Analyzer y la salida de IACA para el tipo de referencia es:

Throughput Analysis Report -------------------------- Block Throughput: 1.72 Cycles Throughput Bottleneck: Dependency chains Loop Count: 35 Port Binding In Cycles Per Iteration: -------------------------------------------------------------------------------------------------- | Port | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------------------------- | Cycles | 1.0 0.0 | 1.0 | 1.5 1.5 | 1.5 1.5 | 0.0 | 1.0 | 1.0 | 0.0 | -------------------------------------------------------------------------------------------------- DV - Divider pipe (on port 0) D - Data fetch pipe (on ports 2 and 3) F - Macro Fusion with the previous instruction occurred * - instruction micro-ops not bound to a port ^ - Micro Fusion occurred # - ESP Tracking sync uop was issued @ - SSE instruction followed an AVX256/AVX512 instruction, dozens of cycles penalty is expected X - instruction not supported, was not accounted in Analysis | Num Of | Ports pressure in cycles | | | Uops | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 | ----------------------------------------------------------------------------------------- | 1* | | | | | | | | | mov r9, rcx | 2^ | | | 0.5 0.5 | 0.5 0.5 | | 1.0 | | | cmp edx, dword ptr [r9+0x8] | 0*F | | | | | | | | | jnb 0x22 | 1 | | | | | | | 1.0 | | movsxd r10, edx | 1 | | | 0.5 0.5 | 0.5 0.5 | | | | | mov r9, qword ptr [r9+r10*8+0x10] | 2^ | 1.0 | | 0.5 0.5 | 0.5 0.5 | | | | | add eax, dword ptr [r9+0x8] | 1 | | 1.0 | | | | | | | inc edx | 1* | | | | | | | | | cmp edx, r8d | 0*F | | | | | | | | | jl 0xffffffffffffffe6 Total Num Of Uops: 9

Para el tipo de valor :

Throughput Analysis Report -------------------------- Block Throughput: 1.74 Cycles Throughput Bottleneck: Dependency chains Loop Count: 26 Port Binding In Cycles Per Iteration: -------------------------------------------------------------------------------------------------- | Port | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------------------------- | Cycles | 1.0 0.0 | 1.0 | 1.0 1.0 | 1.0 1.0 | 0.0 | 1.0 | 1.0 | 0.0 | -------------------------------------------------------------------------------------------------- DV - Divider pipe (on port 0) D - Data fetch pipe (on ports 2 and 3) F - Macro Fusion with the previous instruction occurred * - instruction micro-ops not bound to a port ^ - Micro Fusion occurred # - ESP Tracking sync uop was issued @ - SSE instruction followed an AVX256/AVX512 instruction, dozens of cycles penalty is expected X - instruction not supported, was not accounted in Analysis | Num Of | Ports pressure in cycles | | | Uops | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 | ----------------------------------------------------------------------------------------- | 1* | | | | | | | | | mov r9, rcx | 2^ | | | 1.0 1.0 | | | 1.0 | | | cmp edx, dword ptr [r9+0x8] | 0*F | | | | | | | | | jnb 0x1e | 1 | | | | | | | 1.0 | | movsxd r10, edx | 2 | 1.0 | | | 1.0 1.0 | | | | | add eax, dword ptr [r9+r10*4+0x10] | 1 | | 1.0 | | | | | | | inc edx | 1* | | | | | | | | | cmp edx, r8d | 0*F | | | | | | | | | jl 0xffffffffffffffea Total Num Of Uops: 8

Por lo tanto, hay una ligera ventaja para el tipo de referencia (1,72 ciclos por bucle frente a 1,74 ciclos)

No soy un experto en descifrar la salida de IACA, pero supongo que está relacionado con el uso del puerto (mejor distribuido para el tipo de referencia entre 2-3)

El "puerto" son unidades de micro ejecución en la CPU. Para Skylake, por ejemplo, se dividen de esta manera (de las tablas de instrucciones de Agner optimizan los recursos )

Port 0: Integer, f.p. and vector ALU, mul, div, branch Port 1: Integer, f.p. and vector ALU Port 2: Load Port 3: Load Port 4: Store Port 5: Integer and vector ALU Port 6: Integer ALU, branch Port 7: Store address

Parece una optimización de micro-instrucción muy sutil (uop), pero no puede explicar por qué.

Tenga en cuenta que puede mejorar el código para el bucle de esta manera:

[Benchmark] public int ValueTypeSum() { var sum = 0; // NOTE: Caching the array to a local variable (that will avoid the reload of the Length inside the loop) var arr = _valueTypeData; // NOTE: checking against `array.Length` instead of `Size`, to completely remove the ArrayOutOfBound checks for (var i = 0; i < arr.Length; i++) { sum += arr[i].Value; } return sum; }

El bucle estará un poco mejor optimizado, y también debería tener resultados más consistentes.


Miré el desmontaje en .NET Core 2.1 x64.

El código de tipo de referencia se ve óptimo para mí. El código de máquina carga cada referencia de objeto y luego carga el campo de cada instancia.

Las variantes de tipo de valor tienen una verificación de rango de matriz. La clonación en bucle no tuvo éxito. Esta comprobación de rango se produce porque el límite superior del bucle es Size . Debe ser array.Length para que el JIT pueda reconocer este patrón y no generar una verificación de rango.

Esta es la versión ref. He marcado el bucle del núcleo. El truco para encontrar el bucle central es encontrar primero el salto hacia atrás hasta la parte superior del bucle.

Esta es la variante de valor:

El jae es el control de rango.

Así que esto es una limitación JIT. Si le importa esto, abra un problema de GitHub en el repositorio de Coreclr y dígales que la clonación de bucles falló aquí.

El JIT no legado en 4.7.2 tiene el mismo comportamiento de verificación de rango. El código generado se ve igual para la versión ref:

No he mirado el código JIT heredado, pero asumo que no logra eliminar ninguna verificación de rango. Creo que no es compatible con la clonación de bucle.