Indizadores en C # debajo del capó: indexación mejor que Dow Jones

Buen dia En este artículo, propongo familiarizarme con los indexadores de varios tipos. Veamos el código del lenguaje ensamblador para estos indexadores y las características de cada instrucción en su velocidad. También ofreceré algunas conclusiones obvias. Pero qué usar exactamente en su situación particular depende de usted si sacrifica la conveniencia por la velocidad o viceversa.

imagen

Métricas


El código del lenguaje ensamblador se proporciona para sistemas de 64 bits. Se eligieron las siguientes métricas como métricas para cada instrucción: el número de microoperaciones fusionadas, el número total de microoperaciones, el retraso, el rendimiento y, por supuesto, el número de instrucciones. No di ningún número en su conjunto para el indexador, porque la situación puede variar según cómo trabaje con el tipo indexado y afectar la memoria caché de manera diferente.

A continuación se muestra un breve resumen de la terminología, sin profundizar, solo conceptos conceptuales. Mi objetivo era describir todo lo más simple posible, para una comprensión común.

La microoperación (uop) es una operación determinada en la que consiste cada instrucción. El concepto de microoperaciones se utiliza para optimizaciones tales como fusión, almacenamiento en caché y reordenamiento. Entonces, por ejemplo, la instrucción MOV consiste en 1 micro-operación, mientras que la instrucción XCHG en dos registros consiste en 3 micro-operaciones (el enfoque es a través de una "variable temporal", es decir, un registro interno, gracias leotsarev por la actualización), la instrucción XCHG sobre el registro y la memoria consta de 8 microoperaciones.

Microoperaciones fusionadas (Uops fusionadas) : como se mencionó anteriormente, la fusión de microoperaciones es una de las optimizaciones. Consiste en reemplazar dos microoperaciones por una más compleja.

La latencia es el número de medidas después de las cuales los datos utilizados en esta instrucción estarán disponibles para ser utilizados por otra instrucción.

Rendimiento (rendimiento recíproco) : el número de ciclos de reloj necesarios para ejecutar una instrucción, siempre que se ejecute una secuencia de instrucciones idénticas y operen con datos independientes.

Según estos indicadores, puede evaluar el rendimiento de un conjunto particular de instrucciones. Tenga en cuenta que solo podemos "evaluar", el rendimiento real depende de muchos factores, como la memoria caché de aciertos o errores, la dependencia de datos, etc.

Estas cifras son para la arquitectura del procesador Intel Skylake-X. Esto corresponde a mi procesador Intel Core i7-6700.

También vale la pena recordar que fastcall para sistemas de 64 bits proporciona la transferencia de no 2, sino 4 parámetros en los registros (rcx, rdx, r8, r9).

Indizadores en números


1. Indizador de matriz


Consideraremos los siguientes métodos como ejemplo:

public int IndexerArray(int index, int[] array) { return array[index]; } 

Considere el código de idioma del ensamblador para este fragmento.

 1. cmp edx,dword ptr [r8+8] 2. jae 00007ff9`07288c78 3. movsxd rax,edx 4. mov eax,dword ptr [r8+rax*4+10h] 

La primera línea verifica si el índice va más allá de los límites de la matriz. La segunda línea arroja una excepción si sale. A continuación, calculamos la posición del elemento en la matriz. Los primeros campos de la matriz son información de servicio, por lo que debemos omitirlos (10 h adicionales = 16 bytes).

Información sobre instrucciones:
NoUops fusionadosUops totalesLatenciaRendimiento recíproco
11210.5 0.5
2111-2
31110.25
4 41120.5 0.5



2. Lista de favoritos <>


Código de tinta:
 public int IndexerList(int index, List<int> list) { return list[index]; } 


Código de lenguaje ensamblador:

 1. cmp edx,dword ptr [r8+10h] 2. jae M00_L00 3. mov rax,qword ptr [r8+8] 4. cmp edx,dword ptr [rax+8] 5. jae 00007ff9`07268f56 6. movsxd rdx,edx 7. mov eax,dword ptr [rax+rdx*4+10h] ret M00_L00 call System.ThrowHelper.ThrowArgumentOutOfRange_IndexException() 

Claramente hay más instrucciones aquí. Se ve claramente que el indexador de hoja envuelve el indexador de matriz. Un punto interesante es que la verificación para ir más allá de los límites de la matriz se realiza dos veces. Entonces, la primera instrucción verifica si el índice va más allá de los bordes de la hoja. Si lo hace, saltamos (instrucción 2) a una llamada muy obvia, lanzando una excepción si va más allá de los límites de la matriz. Esta verificación de borde utiliza el campo interno de la hoja, que es el segundo en orden (desplazamiento de 10 h (16) bytes desde el comienzo del tipo, 8 al puntero a la tabla de métodos y 8 al enlace a la matriz interna, el primer campo). En la tercera línea, colocamos en el registro rax la dirección de la matriz interna: el primer campo (por analogía, un desplazamiento de 8 bytes es un puntero a la tabla de métodos). Esto es seguido por la sección ya familiar: la referencia de índice para la matriz (líneas 4 - 7). Aquí, para verificar los límites, se utiliza el campo interno de la matriz.
Traté de eliminar cosas que no están directamente relacionadas con la indexación, pero aquí vale la pena dejar ret para que no parezca que habrá una excepción al final de cada llamada al elemento de hoja: D

Por cierto, para no atormentarlo con especulaciones, familiarícese con la implementación de la hoja por referencia . La conversión de tipos a ints sin signo se utiliza para reducir el número de comparaciones.

Como resultado, obtenemos 7 instrucciones para acceder con éxito al índice, que es 3 más que en la matriz.

Información sobre instrucciones:
NoUops fusionadosUops totalesLatenciaRendimiento recíproco
11210.5 0.5
2111-2
31120.5 0.5
4 41210.5 0.5
5 5111-2
6 61110.25
7 71120.5 0.5


Nuevo - Span <>


Disco:

 public int IndexerSpan(int index, Span<int> span) { return span[index]; } 

Y en lenguaje ensamblador:

 1. cmp edx,dword ptr [r8+8] 2. jae 00007ff9`07278f69 3. mov rax,qword ptr [r8] 4. movsxd rdx,edx 5. mov eax,dword ptr [rax+rdx*4] 

Cuando se anunciaron los tramos, nos prometieron que se hicieron sabiamente, con el apoyo del tiempo de ejecución. Y no mintieron qué decir. De hecho, difiere de la matriz clásica en una sola instrucción, un paso adicional para acceder a la dirección. A juzgar por este código, la dirección de la ubicación de la memoria está oculta dentro del intervalo, donde se encuentran los elementos, que obtenemos en la línea 3. Esta puede ser una dirección a un lugar específico en una matriz, línea o un trozo de memoria en la pila.
Haga clic aquí para obtener una introducción al indexador Span por diversión. Puede notar que hay 2 implementaciones diferentes, dependiendo de la variable de entorno. PROJECTN es el nombre en clave de la primera versión de .NET Native para UWP. Por lo tanto, estamos más interesados ​​en la segunda versión del indexador. Ella está etiquetada [intrínseca] . Además, si observa la clase insegura estática utilizada en la implementación de este indexador, puede encontrar información de que las implementaciones de la mayoría de los métodos en este archivo están representadas como intrínsecas .

Las llamadas a métodos o referencias a campos marcados con el atributo [Intrínseco] tienen soporte desde el tiempo de ejecución.

En CoreCLR , los cuerpos de dichos métodos son reemplazados por EE (motor de ejecución) con código inseguro (inseguro). Si necesita más detalles, puede comenzar a cavar con el método getILIntrinsicImplementationForUnsafe .

Información sobre cómo funciona esto en CoreRT (que me interesa un poco),
puede comenzar a buscar en Internal.IL.Stubs.UnsafeIntrinsics .

Dado el apoyo de Raintime, para comprender qué sucederá exactamente detrás de escena, tiene sentido mirar las instrucciones en el lenguaje ensamblador, lo cual hicimos.

Información sobre instrucciones:
NoUops fusionadosUops totalesLatenciaRendimiento recíproco
11210.5 0.5
2111-2
31120.5 0.5
4 41110.25
5 51120.5 0.5


Todos los indexadores dependen en gran medida de los datos: las instrucciones utilizan los resultados de los anteriores. No hay resultados inusuales aquí, y no debería haberlos. Pero ahora la sobrecarga que aparece en este o aquel caso es clara. Algunos hallazgos obvios. Si el algoritmo involucra accesos muy frecuentes por índice, entonces tiene sentido pensar en reemplazar la hoja con una matriz. Si la llamada no es muy frecuente, puede ser más conveniente usar una hoja que proporcione una API muy conveniente y no tenga una sobrecarga tan grande (le recuerdo que controle las extensiones de la matriz interna).

Ahora tratemos de ver las diferentes formas con las que podemos especificar una matriz bidimensional: una matriz de matrices (matriz dentada) y una matriz multidimensional (matriz multidimensional).

4. Matriz multidimensional


Código Sharp:

 public int IndexerDimensionalArray(int index1, int index2, int[,] demensionalArray) { return demensionalArray[index1, index2]; } 

Lenguaje ensamblador:

 1. mov eax,edx 2. sub eax,dword ptr [r9+18h] 3. cmp eax,dword ptr [r9+10h] 4. jae 00007ff9`00098fe6 5. mov edx,r8d 6. sub edx,dword ptr [r9+1Ch] 7. cmp edx,dword ptr [r9+14h] 8. jae 00007ff9`00098fe6 9. mov ecx,dword ptr [r9+14h] 10. imul rcx,rax 11. mov rax,rdx 12. add rax,rcx 13. mov eax,dword ptr [r9+rax*4+20h] 

En principio, todo es comprensible: 2 verifica los límites de la matriz, luego calcula el índice y retrocede. Esta matriz se almacena en la memoria en un fragmento.

Información sobre instrucciones:
NoUops fusionadosUops totalesLatenciaRendimiento recíproco
1110-10.25
2120.5 0.5
31210.5 0.5
4 4111-2
5 5110-10.25
6 6120.5 0.5
7 71210.5 0.5
8111-2
9 91120.5 0.5
101131
11110-10.25
121110.25
131120.5 0.5



5. Matriz de matrices (matriz irregular)


 public int IndexerJaggedArray(int index, int index2, int[][] jaggedArray) { return jaggedArray[index][index2]; } 

Ensamblador:

 1. cmp edx,dword ptr [r9+8] 2. jae 00007ff9`00098f95 3. movsxd rax,edx 4. mov rax,qword ptr [r9+rax*8+10h] 5. cmp r8d,dword ptr [rax+8] 6. jae 00007ff9`00098f95 7. movsxd rdx,r8d 8. mov eax,dword ptr [rax+rdx*4+10h] 

Y lo más interesante: tenemos menos instrucciones que con un tipo especialmente diseñado para multidimensionalidad.

Información sobre instrucciones:
NoUops fusionadosUops totalesLatenciaRendimiento recíproco
11210.5 0.5
2111-2
31110.25
4 41120.5 0.5
5 51210.5 0.5
6 6111-2
7 71110.25
81120.5 0.5


Pero sobre los últimos 2 ejemplos, le aconsejo que no se apresure a sacar conclusiones. Debido al hecho de que una matriz bidimensional es de un solo tipo, que se inicializa 1 vez, la memoria para toda la matriz se asigna en un fragmento grande. Esto proporcionará una mejor memoria caché, que puede cambiar fundamentalmente la situación. En una matriz de matrices, la memoria para cada matriz se asignará por separado, por lo que es probable que las matrices se asignen en la memoria y se ingresen en los lugares más adecuados para ellas.

Sin embargo, quizás para algunos, este comportamiento será más aceptable. Quizás en algunas situaciones se sabe que la vida útil de este espécimen será de corta duración. Y para no caer en un montón de objetos grandes (que es una especie de segunda generación para el recolector de basura), donde existe la posibilidad de permanecer durante mucho tiempo, mucho más de lo que nos gustaría. O, después de algún tiempo, queremos trabajar solo con ciertas líneas, y todo lo demás se puede borrar. Además, se planea trabajar con el tipo haciendo referencia a elementos inconsistentes aleatorios, cuando el caché no puede funcionar normalmente.

Además, cuando se usa una matriz de matrices, es más probable que no provoque que el recolector de basura se compacte, sino que lo haga con un barrido. Recordatorio: al fragmentar la memoria, la cantidad total de espacio libre puede ser suficiente para un nuevo objeto, pero no hay un área libre continua de la cantidad requerida. En este caso, se realiza la compactación: mover objetos con el objetivo de desfragmentar. Si somos capaces de recoger un tramo continuo de memoria libre para un nuevo objeto, simplemente podemos ingresar el objeto en este espacio libre. Esto se llama barrido.

Espero que esta información lo ayude a sacar las conclusiones correctas y a fundamentar su opinión en la discusión sobre qué usar.

Source: https://habr.com/ru/post/450428/


All Articles