No hace mucho tiempo, tuve otra conversación con un colega sobre un tema eterno: "por referencia o por valor". Como resultado, surgió este artículo. En él, quiero presentar los resultados de mi investigación sobre este y otros temas relacionados. A continuación se considerará:
- Los registros y su propósito al llamar a funciones.
- Transferencia y devolución de tipos y estructuras simples.
- Cómo el paso por referencia y por valor afecta la optimización del cuerpo de la función por parte del compilador.
- Cómo se usa el espacio en llamadas a múltiples funciones.
- El mecanismo de las llamadas virtuales.
- Optimización de llamadas de cola y recursividad.
- Inicialización de estructuras, matrices y vectores.
Precaución El artículo contiene una gran cantidad de C ++ y código de ensamblador ( Intel ASM con comentarios), así como muchas tablas con clasificaciones de rendimiento. Todo lo escrito es relevante para x86-64 System V ABI , que se utiliza en todos los sistemas operativos modernos de Unix, por ejemplo, Linux y macOS.
La información se tomó de la interfaz binaria de la aplicación System V para el documento x86-64 . Se obtuvieron listados de ensambladores para clang 5.0.0 x86-64 con las banderas -O3 -std=c++1z -march=sandybridge
(usando el sitio https://godbolt.org ). Se realizaron clasificaciones de rendimiento para el procesador Intel® Xeon® E5-2660 2.20GHz .
Contenido
Registros en x86-64
Todos los datos se almacenan en la RAM. Para acelerar el trabajo con él, se utilizan cachés multinivel. Pero para cambiar los datos, de una forma u otra, se utilizan registros ( discusión en los comentarios ). A continuación se muestra una breve descripción de los registros más utilizados en la arquitectura x86-64.
- 16 registros de uso general:
rax, rbx, rcx, rdx, rbp, rsi, rdi, rsp
y también r8-r15
. El tamaño de cada uno de ellos es de 64 bits (8 bytes). Para acceder a los 32 bits inferiores (4 bytes), e
el prefijo e
lugar de r
( rax
→ eax
). Solo se admiten operaciones de enteros no vectoriales. rip
(puntero de instrucción) indica la instrucción que se ejecutará a continuación. Varios datos constantes que se encuentran en la sección de memoria con instrucciones pueden leerse en un desplazamiento relativo a la rip
.rsp
(puntero de pila) apunta al último elemento de la pila. La pila crece hacia direcciones más bajas. Empujar algo sobre la pila reduce el valor de rsp
.- 16 SSE registra 128 bits de tamaño:
xmm0 - xmm15
. Si se admite el modo AVX
, se refieren a los 128 bits inferiores de los ymm0 - ymm15
cada uno de los cuales tiene un tamaño de 256 bits. Para operaciones vectoriales o no enteras, los datos primero deben cargarse en estos registros.
Pasando parámetros
Esta sección proporciona una descripción algo abreviada y simplificada del algoritmo para distribuir argumentos entre registros / pila. Para una descripción completa, vea la página 17 "Sistema V ABI".
Introducimos varias clases de objetos:
- INTEGER - Tipos integrales colocados en registros generales. Estos son
bool
, char
, int
, etc. - SSE son números de coma flotante que se ajustan a un registro vectorial Estos son
float
y double
. - MEMORIA - objetos pasados a través de la pila.
Para unificar la descripción, el __int128
y complex
tipos complex
__int128
representan como estructuras de dos campos:
struct __int128 { int64 low, high; }; struct complexT { T real, imag; };
Al principio, cada argumento de función se clasifica:
- Si el tipo es mayor a 128 bits o tiene campos no alineados, entonces es MEMORIA .
- Si hay un destructor no trivial, un constructor de copia, métodos virtuales, clases base virtuales, entonces se pasa a través de un "enlace transparente". El objeto se reemplaza con un puntero de tipo INTEGER .
- Los agregados, y estas son estructuras y matrices, se analizan en piezas de 8 bytes .
- Si hay un campo de tipo MEMORIA en la pieza, entonces toda la pieza es MEMORIA .
- Si hay un campo de tipo INTEGER , toda la pieza es INTEGER .
- De lo contrario, toda la pieza de SSE .
- Si hay una pieza de tipo MEMORY , entonces todo el argumento es MEMORY .
- Los tipos
complex long double
utilizan un conjunto especial de registros x87 FPU
y son del tipo MEMORIA . - Los
__m256
, __m128
y __float128
son de tipo SSE .
Después de la clasificación, todos los fragmentos de 8 bytes (en un fragmento puede haber varios campos de la estructura o elementos de la matriz) se distribuyen en registros:
- MEMORIA se pasan a través de la pila.
- Los INTEGER se transmiten a través del siguiente registro gratuito
rdi, rsi, rdx, rcx, r8, r9
en ese orden. - Las SSE se transmiten a través del siguiente registro gratuito
xmm0 - xmm7
.
Los argumentos se consideran de izquierda a derecha. Los argumentos que no tenían suficientes registros se pasan a través de la pila. Si alguna parte del argumento no tenía un registro, entonces todo el argumento se pasa a través de la pila.
Los valores de retorno son los siguientes:
- Los tipos de MEMORIA se devuelven a través de la pila. La función de llamada proporciona su lugar y la dirección de su comienzo se pasa a través de
rdi
como si fuera el primer argumento de la función. A su regreso, esta dirección debe devolverse a través de rax
. El primer argumento original se pasará, respectivamente, como el segundo, y así sucesivamente. - El fragmento INTEGER se devuelve a través del siguiente registro gratuito
rax, rdx
. - El fragmento SSE se devuelve a través del siguiente registro gratuito
xmm0, xmm1
. Estos registros se utilizan tanto para recibir como para devolver valores.
Una tabla dinámica con registros y su propósito es muy útil al leer ensamblador:
Registrarse | Cita |
---|
rax | Registro temporal, devuelve el primer resultado (ret 1) INTEGER . |
rbx | Pertenece a la función de llamada, no debe cambiarse en el momento de la devolución. |
rcx | Pasando el cuarto (4) argumento INTEGER . |
rdx | Pasando el tercer (3) argumento INTEGER , devolviendo el segundo resultado (ret 2) INTEGER . |
rsp | Puntero a la pila. |
rbp | Pertenece a la función de llamada, no debe cambiarse en el momento de la devolución. |
rsi | Pasando el segundo (2) argumento INTEGER . |
rdi | Pasando el primer (1) argumento INTEGER . |
r8 | Pasando el quinto (5) argumento INTEGER . |
r9 | Pasando el sexto (6) argumento INTEGER . |
r10-r11 | Registros temporales. |
r12-r15 | Pertenece a la función de llamada, no debe cambiarse en el momento de la devolución. |
xmm0-xmm1 | Pase y devuelva el primer y el segundo argumento SSE . |
xmm2-xmm7 | Al pasar los argumentos SSE tercero a sexto. |
xmm8-xmm15 | Registros temporales. |
Los registros que pertenecen a la función de llamada no deben usarse, o sus valores deben almacenarse en algún lugar, por ejemplo, en la pila, y luego restaurarse.
Ejemplos simples
A menos que se indique explícitamente lo contrario, todas las funciones utilizadas se marcaron como NOINLINE
. Suponemos que el cuerpo de la función se encuentra en el archivo cpp y que LTO está deshabilitado. Además, todos los resultados de la función se transfieren a una función NOINLINE
vacía para evitar que el optimizador elimine todo el código.
#define NOINLINE __attribute__((noinline)) #define INLINE static __attribute__((always_inline))
Considera algo simple.
double foo(int8_t a, int16_t b, int32_t c, int64_t d, float x, double y) { return a + b + c + d + x + y; } ... auto result = foo(1, 2, 3, 4, 5, 6);
Los parámetros se pasan así:
Nombre | Registrarse | Nombre | Registrarse | Resultado |
---|
un | rdi | d | rcx | xmm0 |
b | rsi | x | xmm0 | |
c | rdx | y | xmm1 | |
Considere el código generado con más detalle.
foo(signed char, short, int, long, float, double): add edi, esi # a b. add edi, edx # c. movsxd rax, edi # rax, 64 . add rax, rcx # d. vcvtsi2ss xmm2, xmm2, rax # float xmm2. vaddss xmm0, xmm2, xmm0 # x, 's' vaddss single precision. vcvtss2sd xmm0, xmm0, xmm0 # double. vaddsd xmm0, xmm0, xmm1 # y, 'd' vaddsd double precision. ret # , , , rsp 8. .LCPI1_0: # . .long 1084227584 # float 5 .LCPI1_1: .quad 4618441417868443648 # double 6 main: # @main sub rsp, 24 # . vmovss xmm0, dword ptr [rip + .LCPI1_0] # xmm0 = mem[0],zero,zero,zero vmovsd xmm1, qword ptr [rip + .LCPI1_1] # xmm1 = mem[0],zero mov edi, 1 mov esi, 2 mov edx, 3 mov ecx, 4 call foo(signed char, short, int, long, float, double) # call , rsp 8, . vmovsd qword ptr [rsp + 16], xmm0 # .
Si pasa parámetros de tipos simples a una función, debe esforzarse para que no se pasen por los registros.
Considere varios ejemplos de agregados. Las matrices se pueden considerar como estructuras con varios campos.
struct St { double a, b; }; double foo(St s) { return sa + sb; } ... St s{1, 2}; auto result = foo(s);
Nombre | Registrarse | Nombre | Registrarse | Resultado |
---|
sa | xmm0 | sb | xmm1 | xmm0 |
Parece que nada impide empujar dos double
en un registro xmm
a la vez. Pero, por desgracia, el algoritmo de distribución solo funciona en fragmentos de ocho bytes.
foo(St): # @foo(St) vaddsd xmm0, xmm0, xmm1 # , . ret .LCPI1_0: .quad 4607182418800017408 # double 1 .LCPI1_1: .quad 4611686018427387904 # double 2 main: # @main sub rsp, 24 # . vmovsd xmm0, qword ptr [rip + .LCPI1_0] # xmm0 = mem[0],zero vmovsd xmm1, qword ptr [rip + .LCPI1_1] # xmm1 = mem[0],zero call foo(St) vmovsd qword ptr [rsp + 16], xmm0 # double .
Si agrega otro campo double
, toda la estructura se pasará a través de la pila, ya que su tamaño superará los 128 bytes.
struct St { double a, b, c; }; double foo(St s) { return sa + sb + sc; } ... St s{1, 2, 3}; auto result = foo(s);
foo(St): # @foo(St) # , 8 , . , rsp+8. vmovsd xmm0, qword ptr [rsp + 8] # xmm0 = mem[0],zero vaddsd xmm0, xmm0, qword ptr [rsp + 16] vaddsd xmm0, xmm0, qword ptr [rsp + 24] ret .L_ZZ4mainE1s: .quad 4607182418800017408 # double 1 .quad 4611686018427387904 # double 2 .quad 4613937818241073152 # double 3 main: # @main sub rsp, 40 # 40 . # , . , mov . mov rax, qword ptr [rip + .L_ZZ4mainE1s+16] # '3'. mov qword ptr [rsp + 16], rax # '3' . vmovups xmm0, xmmword ptr [rip + .L_ZZ4mainE1s] # xmm0 '1' '2'. vmovups xmmword ptr [rsp], xmm0 # '1' '2 . : 1 = *rsp , 2 = *(rsp+8), 3 = *(rsp+16). call foo(St) vmovsd qword ptr [rsp + 32], xmm0 # double .
Veamos qué sucede si reemplazamos double
con uint64_t
.
struct St { uint64_t a, b; }; uint64_t foo(St s) { return sa + sb; } ... St s{1, 2}; auto result = foo(s);
Nombre | Registrarse | Nombre | Registrarse | Resultado |
---|
sa | rdi | sb | rsi | rax |
foo(St): # @foo(St) lea rax, [rdi + rsi] ret main: # @main sub rsp, 24 mov edi, 1 mov esi, 2 call foo(St) mov qword ptr [rsp + 16], rax
El resultado es notablemente más compacto. Puede leer más información sobre por qué se usa la instrucción lea
lugar de add
, por ejemplo, aquí: https://stackoverflow.com/a/6328441/1418863
Si agrega otro campo, entonces, como en el ejemplo con double
, la estructura se pasará a través de la pila. El código, por cierto, será casi idéntico, incluso la carga en la pila se realizará a través de registros xmm
.
Considera algo más interesante.
struct St { float a, b, c, d; }; St foo(St s1, St s2) { return {s1.a + s2.a, s1.b + s2.b, s1.c + s2.c, s1.d + s2.d}; } ... St s1{1, 2, 3, 4}, s2{5, 6, 7, 8}; auto result = foo(s1, s2);
Nombre | Registrarse | Nombre | Registrarse | Resultado |
---|
s1.a | xmm0 | s1.b | xmm0 | xmm0, xmm1 |
s1.c | xmm1 | s1.d | xmm1 | |
s2.a | xmm2 | s2.b | xmm2 | |
s2.c | xmm3 | s2.d | xmm3 | |
Se xmm
dos campos float
en cada registro xmm
.
foo(St, St): # @foo(St, St) # vaddps float . vaddps xmm0, xmm0, xmm2 vaddps xmm1, xmm1, xmm3 ret .LCPI1_0: .long 1065353216 # float 1 .long 1073741824 # float 2 .zero 4 .zero 4 # LCPI1_1 - LCPI1_3. ... main: # @main sub rsp, 24 # , . vmovapd xmm0, xmmword ptr [rip + .LCPI1_0] # xmm0 = <1,2,u,u> vmovapd xmm1, xmmword ptr [rip + .LCPI1_1] # xmm1 = <3,4,u,u> vmovaps xmm2, xmmword ptr [rip + .LCPI1_2] # xmm2 = <5,6,u,u> vmovaps xmm3, xmmword ptr [rip + .LCPI1_3] # xmm3 = <7,8,u,u> call foo(St, St) # , . xmm 256 , 128 a b, - c d. vunpcklpd xmm0, xmm0, xmm1 # xmm0 = xmm0[0],xmm1[0] vmovupd xmmword ptr [rsp + 8], xmm0
Si la estructura no tuviera 4, sino tres campos, entonces el código de función sería similar, excepto para reemplazar la segunda instrucción vaddss
con vaddss
, que agrega solo los primeros 64 bits del registro.
struct St { int32_t a, b, c, d; }; St foo(St s1, St s2) { return {s1.a + s2.a, s1.b + s2.b, s1.c + s2.c, s1.d + s2.d}; } ... St s1{1, 2, 3, 4}, s2{5, 6, 7, 8}; auto result = foo(s1, s2);
Nombre | Registrarse | Nombre | Registrarse | Resultado |
---|
s1.a | rdi | s1.b | rdi | rax, rdx |
s1.c | rsi | s1.d | rsi | |
s2.a | rdx | s2.b | rdx | |
s2.c | rcx | s2.d | rcx | |
foo(St, St): # @foo(St, St) lea eax, [rdx + rdi] movabs r8, -4294967296 # 0xFFFFFFFF00000000 . and rdi, r8 add rdi, rdx and rdi, r8 or rax, rdi lea edx, [rcx + rsi] # , add. and rsi, r8 add rsi, rcx and rsi, r8 or rdx, rsi ret main: # @main sub rsp, 24 movabs rdi, 8589934593 # . movabs rsi, 17179869187 movabs rdx, 25769803781 movabs rcx, 34359738375 call foo(St, St) mov qword ptr [rsp + 8], rax # . mov qword ptr [rsp + 16], rdx
Un poco de magia ocurre dentro de la función, pero el principio es bastante claro. Cada par de números de 32 bits está empaquetado en un registro de 64 bits. Los reembolsos se realizan de la misma manera.
Veamos qué sucede si comenzamos a mezclar tipos de campo, pero de manera que en trozos de 8 bytes sean de la misma clase.
struct St { int32_t a, b; float c, d; }; St foo(St s1, St s2) { return {s1.a + s2.a, s1.b + s2.b, s1.c + s2.c, s1.d + s2.d}; } ... St s1{1, 2, 3, 4}, s2{5, 6, 7, 8}; auto result = foo(s1, s2);
Nombre | Registrarse | Nombre | Registrarse | Resultado |
---|
s1.a | rdi | s1.b | rdi | rax, xmm0 |
s1.c | xmm0 | s1.d | xmm0 | |
s2.a | rsi | s2.b | rsi | |
s2.c | xmm1 | s2.d | xmm1 | |
foo(St, St): # @foo(St, St) lea eax, [rsi + rdi] # . movabs rcx, -4294967296 and rdi, rcx add rdi, rsi and rdi, rcx or rax, rdi vaddps xmm0, xmm0, xmm1 # . ret .LCPI1_0: .long 1077936128 # float 3 .long 1082130432 # float 4 .zero 4 .zero 4 ... main: # @main sub rsp, 24 vmovaps xmm0, xmmword ptr [rip + .LCPI1_0] # xmm0 = <3,4,u,u> vmovaps xmm1, xmmword ptr [rip + .LCPI1_1] # xmm1 = <7,8,u,u> movabs rdi, 8589934593 movabs rsi, 25769803781 call foo(St, St) mov qword ptr [rsp + 8], rax # . vmovlps qword ptr [rsp + 16], xmm0
Pero esto no es interesante, ya que los tipos de campos en cada fragmento de 8 bytes son los mismos. Baraja los campos.
struct St { int32_t a; float b; int32_t c; float d; }; St foo(St s1, St s2) { return {s1.a + s2.a, s1.b + s2.b, s1.c + s2.c, s1.d + s2.d}; } ... St s1{1, 2, 3, 4}, s2{5, 6, 7, 8}; auto result = foo(s1, s2);
Nombre | Registrarse | Nombre | Registrarse | Resultado |
---|
s1.a | rdi | s1.b | rdi | rax, rdx |
s1.c | rsi | s1.d | rsi | |
s2.a | rdx | s2.b | rdx | |
s2.c | rcx | s2.d | rcx | |
Ver párrafo 3.2. Como el fragmento de 8 bytes contiene tanto float
como int
, todo el fragmento será de tipo INTEGER y se pasará en registros generales.
foo(St, St): # @foo(St, St) mov rax, rdx add edx, edi shr rdi, 32 vmovd xmm0, edi mov rdi, rcx add ecx, esi shr rsi, 32 vmovd xmm1, esi shr rax, 32 vmovd xmm2, eax vaddss xmm0, xmm0, xmm2 shr rdi, 32 vmovd xmm2, edi vaddss xmm1, xmm1, xmm2 vmovd eax, xmm0 shl rax, 32 or rdx, rax vmovd eax, xmm1 shl rax, 32 or rcx, rax mov rax, rdx mov rdx, rcx ret main: # @main sub rsp, 24 movabs rdi, 4611686018427387905 # 0x4000000000000001, 32 int, - float. movabs rsi, 4647714815446351875 movabs rdx, 4665729213955833861 movabs rcx, 4683743612465315847 call foo(St, St) mov qword ptr [rsp + 8], rax # 64 . mov qword ptr [rsp + 16], rdx
Aquí puede ver 6 operaciones de turno para extraer campos float
e insertarlos en el registro con el resultado. Así como la ausencia de operaciones vectoriales. En general, es mejor no interferir con los tipos de campo dentro de fragmentos de 8 bytes de la estructura.
Pasar por enlace
Pasar parámetros a través de una referencia constante es similar a pasar un puntero a un objeto. Si el objeto no cabe en los registros, se pasa y se devuelve a través de la pila. Veamos cómo sucede esto. Para el realismo, considere la estructura de un punto tridimensional.
struct Point3f { float x, y, z; }; struct Point3d { double x, y, z; }; Point3f scale(Point3f p) { return {px * 2, py * 2, pz * 2}; } Point3f scaleR(const Point3f& p) { return {px * 2, py * 2, pz * 2}; } Point3d scale(Point3d p) { return {px * 2, py * 2, pz * 2}; } Point3d scaleR(const Point3d& p) { return {px * 2, py * 2, pz * 2}; }
Compara el código de función. Se utilizarán principalmente nuevos registros xmm
, por lo que la lógica es comprensible.
scale(Point3f): # @scale(Point3f) # , x, y xmm0, z - xmm1, . vaddps xmm0, xmm0, xmm0 vaddss xmm1, xmm1, xmm1 ret scaleR(Point3f const&): # @scaleR(Point3f const&) # rdi, . xmm0, xmm1. vmovsd xmm0, qword ptr [rdi] # xmm0 = mem[0],zero vaddps xmm0, xmm0, xmm0 vmovss xmm1, dword ptr [rdi + 8] # xmm1 = mem[0],zero,zero,zero vaddss xmm1, xmm1, xmm1 ret scale(Point3d): # @scale(Point3d) # rdi , . [rsp+8, rsp+32). [rsp, rsp+8) . vmovapd xmm0, xmmword ptr [rsp + 8] vaddpd xmm0, xmm0, xmm0 vmovupd xmmword ptr [rdi], xmm0 vmovsd xmm0, qword ptr [rsp + 24] # xmm0 = mem[0],zero vaddsd xmm0, xmm0, xmm0 vmovsd qword ptr [rdi + 16], xmm0 mov rax, rdi # , . ret scaleR(Point3d const&): # @scaleR(Point3d const&) # , [rsp+8, rsp+32), [rsi, rsi+24). vmovupd xmm0, xmmword ptr [rsi] vaddpd xmm0, xmm0, xmm0 vmovupd xmmword ptr [rdi], xmm0 vmovsd xmm0, qword ptr [rsi + 16] # xmm0 = mem[0],zero vaddsd xmm0, xmm0, xmm0 vmovsd qword ptr [rdi + 16], xmm0 mov rax, rdi ret
Ahora veamos el lugar donde se llaman estas funciones.
# scale(Point3f) main: # @main sub rsp, 24 # . vmovaps xmm0, xmmword ptr [rip + .LCPI4_0] # xmm0 = <1,2,u,u> vmovss xmm1, dword ptr [rip + .LCPI4_1] # xmm1 = <3,u,u,u> call scale(Point3f) # scaleR(const Point3f&) main: # @main sub rsp, 24 # rdi, . mov edi, .L_ZZ4mainE1p # <1,2,3,u> call scaleR(Point3f const&) # scale(Point3d) main: # @main sub rsp, 64 # . mov rax, qword ptr [rip + .L_ZZ4mainE1p+16] mov qword ptr [rsp + 16], rax vmovups xmm0, xmmword ptr [rip + .L_ZZ4mainE1p] vmovups xmmword ptr [rsp], xmm0 lea rbx, [rsp + 40] mov rdi, rbx # [rsp+40, rsp+64). call scale(Point3d) .L_ZZ4mainE1p: .quad 4607182418800017408 # double 1 .quad 4611686018427387904 # double 2 .quad 4613937818241073152 # double 3 # scaleR(const Point3d&) main: # @main sub rsp, 64 # . mov rax, qword ptr [rip + .L_ZZ4mainE1p+16] mov qword ptr [rsp + 32], rax vmovups xmm0, xmmword ptr [rip + .L_ZZ4mainE1p] vmovaps xmmword ptr [rsp + 16], xmm0 lea rbx, [rsp + 40] lea rsi, [rsp + 16] # [rsp+16, rsp+40). mov rdi, rbx # [rsp+40, rsp+64). call scaleR(Point3d const&)
Veamos eso si tenemos muchos campos, pero la estructura aún encaja en los registros. Aquí comienza la diversión.
struct St { char d[16]; }; St foo(St s1, St s2) {
Código para una función que toma argumentos por valor.
Nombre | Registrarse | Nombre | Registrarse | Resultado |
---|
s1.d [1: 8] | rdi | s1.d [8:16] | rsi | rax, rdx |
s2.d [1: 8] | rdx | s2.d [8:16] | rcx | |
foo(St, St): # @foo(St, St) mov qword ptr [rsp - 16], rdi mov qword ptr [rsp - 8], rsi mov qword ptr [rsp - 32], rdx mov qword ptr [rsp - 24], rcx mov eax, edx add al, dil mov byte ptr [rsp - 48], al mov r8, rdi shr r8, 8 mov rax, rdx shr rax, 8 add al, r8b mov byte ptr [rsp - 47], al mov r8, rdi shr r8, 16 mov rax, rdx shr rax, 16 add al, r8b mov byte ptr [rsp - 46], al mov r8, rdi shr r8, 24 mov rax, rdx shr rax, 24 add al, r8b mov byte ptr [rsp - 45], al mov r8, rdi shr r8, 32 mov rax, rdx shr rax, 32 add al, r8b mov byte ptr [rsp - 44], al mov r8, rdi shr r8, 40 mov rax, rdx shr rax, 40 add al, r8b mov byte ptr [rsp - 43], al mov r8, rdi shr r8, 48 mov rax, rdx shr rax, 48 add al, r8b mov byte ptr [rsp - 42], al shr rdi, 56 shr rdx, 56 add dl, dil mov byte ptr [rsp - 41], dl mov eax, ecx add al, sil mov byte ptr [rsp - 40], al mov rax, rsi shr rax, 8 mov rdx, rcx shr rdx, 8 add dl, al mov byte ptr [rsp - 39], dl shr rsi, 16 shr rcx, 16 add cl, sil mov byte ptr [rsp - 38], cl mov al, byte ptr [rsp - 21] mov cl, byte ptr [rsp - 20] add al, byte ptr [rsp - 5] mov byte ptr [rsp - 37], al add cl, byte ptr [rsp - 4] mov byte ptr [rsp - 36], cl mov al, byte ptr [rsp - 19] mov cl, byte ptr [rsp - 18] add al, byte ptr [rsp - 3] mov byte ptr [rsp - 35], al add cl, byte ptr [rsp - 2] mov byte ptr [rsp - 34], cl mov al, byte ptr [rsp - 17] add al, byte ptr [rsp - 1] mov byte ptr [rsp - 33], al mov rax, qword ptr [rsp - 48] mov rdx, qword ptr [rsp - 40] ret
Sí, aquí todos los argumentos se copian en la pila, después de lo cual se extrae y se agrega un byte a la vez. Como puede ver, hay exactamente 16 instrucciones para add
en la función. GCC, por cierto, en este ejemplo produce un código mucho más compacto, pero aún con copia a través de la pila. ¿Se puede mejorar algo? Pase la estructura por referencia.
St fooR(const St& s1, const St& s2) { }
Nombre | Registrarse | Nombre | Registrarse | Resultado |
---|
s1 | rdi | s2 | rsi | rax, rdx |
fooR(St const&, St const&): # @fooR(St const&, St const&) vmovdqu xmm0, xmmword ptr [rsi] vpaddb xmm0, xmm0, xmmword ptr [rdi] vmovdqa xmmword ptr [rsp - 24], xmm0 mov rax, qword ptr [rsp - 24] mov rdx, qword ptr [rsp - 16] ret
Oh si! Se ve mucho mejor. Podemos cargar 16 elementos de un solo byte a la vez en el registro xmm
y llamar a vpaddb
que los vpaddb
a todos en una sola operación. Después de eso, el resultado se copia en los registros de salida a través de la pila. Puede pensar que puede deshacerse de esta última operación reemplazando el primer argumento con una referencia no constante.
void fooR1(St &s1, const St& s2) { for(int i{}; i < 16; ++i) s1.d[i] += s2.d[i]; }
Nombre | Registrarse | Nombre | Registrarse | Resultado |
---|
s1 | rdi | s2 | rsi | |
fooR1(St&, St const&): # @fooR1(St&, St const&) mov al, byte ptr [rsi] add byte ptr [rdi], al mov al, byte ptr [rsi + 1] add byte ptr [rdi + 1], al mov al, byte ptr [rsi + 2] add byte ptr [rdi + 2], al mov al, byte ptr [rsi + 3] add byte ptr [rdi + 3], al mov al, byte ptr [rsi + 4] add byte ptr [rdi + 4], al mov al, byte ptr [rsi + 5] add byte ptr [rdi + 5], al mov al, byte ptr [rsi + 6] add byte ptr [rdi + 6], al mov al, byte ptr [rsi + 7] add byte ptr [rdi + 7], al mov al, byte ptr [rsi + 8] add byte ptr [rdi + 8], al mov al, byte ptr [rsi + 9] add byte ptr [rdi + 9], al mov al, byte ptr [rsi + 10] add byte ptr [rdi + 10], al mov al, byte ptr [rsi + 11] add byte ptr [rdi + 11], al mov al, byte ptr [rsi + 12] add byte ptr [rdi + 12], al mov al, byte ptr [rsi + 13] add byte ptr [rdi + 13], al mov al, byte ptr [rsi + 14] add byte ptr [rdi + 14], al mov al, byte ptr [rsi + 15] add byte ptr [rdi + 15], al ret
Algo parece haber salido mal. Esto sucedió porque, por defecto, el compilador es muy cuidadoso y sugiere que el programador puede estar fuera de lugar y escribir algo como esto:
char buff[17]; fooR1(*reinterpret_cast<St*>(buff+1), reinterpret_cast<const St*>(buff));
En este caso, buff[i+1] += buff[i]
calcula en cada iteración, es decir, el alias de puntero está disponible. Para indicar al compilador que no se espera un uso tan extraño de la función, existe la palabra clave __restrict .
void fooR2(St & __restrict s1, const St& s2) { }
.
fooR2(St&, St const&): # @fooR2(St&, St const&) vmovdqu xmm0, xmmword ptr [rdi] vpaddb xmm0, xmm0, xmmword ptr [rsi] vmovdqu xmmword ptr [rdi], xmm0 ret
void fooR3(St &__restrict s1, St s2)
, , St foo(St, St)
.
, , void foo(char* __restrict s1, const char* s2, int size)
, __restrict
.
b
a
, , foo
:
St a, b; st(a, b);
Code | Cycles per iteration |
---|
St a, b; st(a, b); | 7.6 |
4 x foo no reuse | 121.9 |
4 x foo | 117.7 |
4 x fooR no reuse | 66.3 |
4 x fooR | 64.6 |
4 x fooR1 | 84.5 |
4 x fooR2 | 20.6 |
4 x foo inline | 51.9 |
4 x fooR inline | 30.5 |
4 x fooR1 inline | 8.8 |
4 x fooR2 inline | 8.8 |
'no reuse' , . auto a2 = foo(a, b); auto a3 = foo(a2, b);
. 'inline' , INLINE , NOINLINE .
fooR1 inline / fooR2 inline
, , , , , foo inline / fooR inline
, . , , .
, , .
struct Point3f { float x, y, z; ~Point3f() {} }; Point3f scale(Point3f p) { return {px * 2, py * 2, pz * 2}; }
rdi
, . , rsi
, .
scale(Point3f): # @scale(Point3f) vmovss xmm0, dword ptr [rsi] # xmm0 = mem[0],zero,zero,zero vaddss xmm0, xmm0, xmm0 vmovss dword ptr [rdi], xmm0 vmovss xmm0, dword ptr [rsi + 4] # xmm0 = mem[0],zero,zero,zero vaddss xmm0, xmm0, xmm0 vmovss dword ptr [rdi + 4], xmm0 vmovss xmm0, dword ptr [rsi + 8] # xmm0 = mem[0],zero,zero,zero vaddss xmm0, xmm0, xmm0 vmovss dword ptr [rdi + 8], xmm0 mov rax, rdi ret
, , ( ) . POD . Point3f scaleR(const Point3f&)
. .
Point3f p{1, 2, 3}; auto result = scale(p); sink(&result);
main: # @main push rbx sub rsp, 48 movabs rax, 4611686019492741120 # . mov qword ptr [rsp + 16], rax mov dword ptr [rsp + 24], 1077936128 lea rbx, [rsp + 32] lea rsi, [rsp + 16] # [rsp+16, rsp+28) mov rdi, rbx # [rsp+32, rsp+44) call scale(Point3f) mov qword ptr [rsp + 8], rbx # [rsp+8, rsp+16) . lea rdi, [rsp + 8] call void sink<Point3f*>(Point3f* const&) xor eax, eax add rsp, 48 pop rbx ret # . mov rdi, rax call _Unwind_Resume
NOINLINE , .
main: # @main push r14 push rbx sub rsp, 56 movabs rax, 4611686019492741120 # [rsp, rsp+12). p. mov qword ptr [rsp], rax mov dword ptr [rsp + 8], 1077936128 # [rsp+24, rsp+36), pTmp. mov eax, dword ptr [rsp + 8] mov dword ptr [rsp + 32], eax mov rax, qword ptr [rsp] mov qword ptr [rsp + 24], rax lea r14, [rsp + 40] lea rbx, [rsp + 24] mov rdi, r14 # [rsp+40, rsp+52), result. mov rsi, rbx # - pTmp. call scale(Point3f) mov rdi, rbx # pTmp. - this. call Point3f::~Point3f() mov qword ptr [rsp + 16], r14 # [rsp+16, rsp+24) result. sink. lea rdi, [rsp + 16] call void sink<Point3f*>(Point3f* const&) lea rdi, [rsp + 40] # result. call Point3f::~Point3f() mov rdi, rsp # p. call Point3f::~Point3f() xor eax, eax add rsp, 56 pop rbx pop r14 ret # . . mov rbx, rax lea rdi, [rsp + 40] # result. call Point3f::~Point3f() mov rdi, rsp # p. call Point3f::~Point3f() mov rdi, rbx call _Unwind_Resume
p
, .
, . .
# Point3f result = scale(scale(Point3f{1, 2, 3})); sub rsp, 24 vmovaps xmm0, xmmword ptr [rip + .LCPI4_0] # xmm0 = <1,2,u,u> vmovss xmm1, dword ptr [rip + .LCPI4_1] # xmm1 = mem[0],zero,zero,zero # xmm0, xmm1 , , ! call scale(Point3f) call scale(Point3f) vmovlps qword ptr [rsp + 8], xmm0 vmovss dword ptr [rsp + 16], xmm1 # Point3f result = scaleR(scaleR(Point3f{1, 2, 3})); sub rsp, 56 # [rsp+24, rsp+36). movabs rax, 4611686019492741120 # 0x400000003F800000 = [2.0f, 1.0f] mov qword ptr [rsp + 24], rax mov dword ptr [rsp + 32], 1077936128 # 0x40400000 = 3.0f lea rdi, [rsp + 24] # . call scaleR(Point3f const&) # [rsp+8, rsp+20). vmovlps qword ptr [rsp + 8], xmm0 vmovss dword ptr [rsp + 16], xmm1 lea rdi, [rsp + 8] # . call scaleR(Point3f const&) vmovlps qword ptr [rsp + 40], xmm0 vmovss dword ptr [rsp + 48], xmm1
, , . , .
# Point3d result = scale(scale(Point3d{1, 2, 3})); sub rsp, 112 # [rsp, rsp+24). vmovaps xmm0, xmmword ptr [rip + .LCPI4_0] # xmm0 = [1.000000e+00,2.000000e+00] vmovaps xmmword ptr [rsp + 32], xmm0 movabs rax, 4613937818241073152 # 0x4008000000000000 = 3.0 mov qword ptr [rsp + 48], rax mov rax, qword ptr [rsp + 48] mov qword ptr [rsp + 16], rax vmovaps xmm0, xmmword ptr [rsp + 32] vmovups xmmword ptr [rsp], xmm0 # [rsp+64, rsp+88). lea rdi, [rsp + 64] # rdi . call scale(Point3d) # [rsp, rsp+24). mov rax, qword ptr [rsp + 80] # z z . mov qword ptr [rsp + 16], rax vmovups xmm0, xmmword ptr [rsp + 64] # [x, y] [x, y] . vmovups xmmword ptr [rsp], xmm0 # [rsp+88, rsp+112). lea rbx, [rsp + 88] mov rdi, rbx call scale(Point3d) # Point3d result = scaleR(scaleR(Point3d{1, 2, 3})); sub rsp, 72 # [rsp, rsp+24), . vmovaps xmm0, xmmword ptr [rip + .LCPI4_0] vmovaps xmmword ptr [rsp], xmm0 movabs rax, 4613937818241073152 mov qword ptr [rsp + 16], rax lea r14, [rsp + 24] mov rsi, rsp # - [rsp, rsp+24). mov rdi, r14 # - [rsp+24, rsp+48). call scaleR(Point3d const&) lea rbx, [rsp + 48] mov rdi, rbx # [rsp+48, rsp+72). mov rsi, r14 # [rsp+24, rsp+48). call scaleR(Point3d const&)
, , , , . – . . , , .
, . , . , , Point3f
, Point3d
– .
Code | Cycles per iteration |
---|
auto r = pf(); | 6.7 |
auto r = scale(pf()); | 11.1 |
auto r = scaleR(pf()); | 12.6 |
auto r = scale(scale(pf())); | 18.2 |
auto r = scaleR(scaleR(pf())); | 18.3 |
auto r = scale(scale(scale(pf()))); | 16.8 |
auto r = scaleR(scaleR(scaleR(pf()))); | 20.2 |
auto r = pd(); | 7.3 |
auto r = scale(pd()); | 11.7 |
auto r = scaleR(pd()); | 11.0 |
auto r = scale(scale(pd())); | 16.9 |
auto r = scaleR(scaleR(pd())); | 14.1 |
auto r = scale(scale(scale(pd()))); | 21.2 |
auto r = scaleR(scaleR(scaleR(pd()))); | 17.2 |
INLINE | 8.1 — 8.9 |
Point3f
struct Point3i { int32_t x, y, z; };
Point3d
struct Point3ll { int64_t x, y, z; };
, . , , , 64 int, . , Point3f
struct Point2ll { int64_t x, y; };
Point3d
struct Point4ll { int64_t x, y, z, a; };
, -.
:
- . .
- , , .
- inline , . , , , . .
optional
std::optional
, boost::optional
, , "x86-64 clang (experimental concepts)" , , MSVC ,
struct Point { float x, y; }; using OptPoint1 = optional<Point>;
struct OptPoint2 { float x, y; union { char _; bool d; };
, OptPoint1
, OptPoint2
– .
OptPoint1 foo(OptPoint1 s) { return Point{s->x + 1, s->y + 1}; } OptPoint2 foo(OptPoint2 s) { return {sx + 1, sy + 1, true}; } ... OptPoint1 s1{Point{1, 2}}; OptPoint2 s2{3, 4, true}; auto result1 = foo(s1); auto result2 = foo(s2);
.LCPI0_0: .long 1065353216 # float 1 foo(std::optional<Point>): # @foo(std::optional<Point>) vmovss xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero # rsi . vaddss xmm1, xmm0, dword ptr [rsi] # x , 1. vaddss xmm0, xmm0, dword ptr [rsi + 4] # y , 1. vmovss dword ptr [rdi], xmm1 # x , rdi , . vmovss dword ptr [rdi + 4], xmm0 # y . mov byte ptr [rdi + 8], 1 # optional::has_value(). mov rax, rdi # . ret .LCPI1_0: .long 1065353216 # float 1 .long 1065353216 # float 1 .zero 4 .zero 4 foo(OptPoint2): # @foo(OptPoint2) vaddps xmm0, xmm0, xmmword ptr [rip + .LCPI1_0] # [x, y] c [1, 1]. xmm0 . mov al, 1 # d, al - 8 rax. ret .LCPI2_0: .long 1077936128 # float 3 .long 1082130432 # float 4 .zero 4 .zero 4 main: # @main push rbx sub rsp, 64 movabs rax, 4611686019492741120 # 0x400000003F800000 x y. mov qword ptr [rsp + 32], rax # x, y . mov byte ptr [rsp + 40], 1 # optional::has_value(). lea rbx, [rsp + 48] lea rsi, [rsp + 32] # . mov rdi, rbx # . call foo(std::optional<Point>) # . vmovaps xmm0, xmmword ptr [rip + .LCPI2_0] # xmm0 = <3,4,u,u> mov edi, 1 # bool d. call foo(OptPoint2) vmovlps qword ptr [rsp + 16], xmm0 # . mov byte ptr [rsp + 24], al # al rax.
, foo
inline
, .
# OptPoint1 foo(OptPoint1) OptPoint1 foo(const OptPoint1&) vmovss xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero vaddss xmm1, xmm0, dword ptr [rsp + 32] vaddss xmm0, xmm0, dword ptr [rsp + 36] vmovss dword ptr [rsp + 8], xmm1 vmovss dword ptr [rsp + 12], xmm0 mov byte ptr [rsp + 16], 1 # OptPoint2 foo(OptPoint2) OptPoint2 foo(const OptPoint2&) vmovsd xmm0, qword ptr [rsp + 48] # xmm0 = mem[0],zero vaddps xmm0, xmm0, xmmword ptr [rip + .LCPI0_1] vmovlps qword ptr [rsp + 8], xmm0 mov byte ptr [rsp + 16], 1
, , , .
: inline
, std::optional
.
, , . . , . , - , , , . .
struct Fn { virtual ~Fn() noexcept = default; virtual int call(int x) const = 0; }; struct Add final : Fn { Add(int a) : a(a) {} int call(int x) const override { return a + x; } int a; }; NOINLINE bool isFixedPoint(const Fn& fn, int x) { return fn.call(x) == x; } int main() { Add add{32}; bool result = isFixedPoint(add, 10); }
- .
Add::call(int) const: # @Add::call(int) const # rdi this , [rdi, rdi+8) , . add esi, dword ptr [rdi + 8] mov eax, esi # rax, eax 32 . ret # Add. 16, RTTI . vtable for Add: .quad 0 .quad typeinfo for Add # RTTI. -8. .quad Fn::~Fn() # , 0 . .quad Add::~Add() .quad Add::call(int) const # , 16 . isFixedPoint(Fn const&, int): # @isFixedPoint(Fn const&, int) push rbx # rbx, , . mov ebx, esi # 32 . mov rax, qword ptr [rdi] # rdi - Fn, this . call qword ptr [rax + 16] # Add::call. cmp eax, ebx # call rax, ebx, . sete al # 8 eax. pop rbx # rbx. ret main: # @main sub rsp, 40 mov qword ptr [rsp + 24], vtable for Add+16 # Add, 16 , , , . RTTI . mov dword ptr [rsp + 32], 32 # add.a . lea rdi, [rsp + 24] # . mov esi, 10 # . call isFixedPoint(Fn const&, int) mov byte ptr [rsp + 15], al # . ... mov rdi, rax # . call _Unwind_Resume mov rdi, rax call _Unwind_Resume
protected
, ( 34-37). NOINLINE , ( false
). NOINLINE , . .
struct Add final { Add(int a) : a(a) {} NOINLINE int call(int x) const { return a + x; } int a; }; template<typename T> NOINLINE bool isFixedPoint(const T& fn, int x) { return fn.call(x) == x; }
Add::call(int) const: # @Add::call(int) const add esi, dword ptr [rdi] # , this rdi , , . mov eax, esi ret bool isFixedPoint<Add>(Add const&, int): # @bool isFixedPoint<Add>(Add const&, int) push rbx mov ebx, esi # Add::call , isFixedPoint. call Add::call(int) const cmp eax, ebx sete al pop rbx ret main: # @main sub rsp, 24 mov dword ptr [rsp + 8], 32 # add.a. lea rdi, [rsp + 8] # , rdi add.a. mov esi, 10 call bool isFixedPoint<Add>(Add const&, int) mov byte ptr [rsp + 7], al ... ret
, NOINLINE .
1000 Add
isFixedPoint
.
Code | Cycles per iteration |
---|
call , isFixedPoint call | 5267 |
call , NOINLINE isFixedPoint | 10721 |
call , INLINE isFixedPoint | 8291 |
call , NOINLINE isFixedPoint | 10571 |
, NOINLINE call , NOINLINE isFixedPoint | 10536 |
, isFixedPoint call | 4505 |
, INLINE call , INLINE isFixedPoint | 4531 |
:
- .
- , .
- , , , inline. .
inline
. inline
- cpp , NOINLINE .inline
.
call
, jmp
.
. clang . , :
double exp_by_squaring(double x, int n, double y = 1) { if (n < 0) return exp_by_squaring(1.0 / x, -n, y); if (n == 0) return y; if (n == 1) return x * y; if (n % 2 == 0) return exp_by_squaring(x * x, n / 2, y); return exp_by_squaring(x * x, (n - 1) / 2, x * y); }
Obtenemos:
.LCPI0_0: .quad 4607182418800017408 # double 1 exp_by_squaring(double, int, double): # @exp_by_squaring(double, int, double) vmovsd xmm2, qword ptr [rip + .LCPI0_0] # xmm2 = mem[0],zero vmovapd xmm3, xmm0 test edi, edi jns .LBB0_4 jmp .LBB0_3 .LBB0_9: # in Loop: Header=BB0_4 Depth=1 shr edi vmovapd xmm3, xmm0 test edi, edi jns .LBB0_4 .LBB0_3: # =>This Inner Loop Header: Depth=1 vdivsd xmm3, xmm2, xmm3 neg edi test edi, edi js .LBB0_3 .LBB0_4: # =>This Inner Loop Header: Depth=1 je .LBB0_7 cmp edi, 1 je .LBB0_6 vmulsd xmm0, xmm3, xmm3 test dil, 1 je .LBB0_9 lea eax, [rdi - 1] shr eax, 31 lea edi, [rdi + rax] add edi, -1 sar edi vmulsd xmm1, xmm3, xmm1 vmovapd xmm3, xmm0 test edi, edi jns .LBB0_4 jmp .LBB0_3 .LBB0_6: vmulsd xmm1, xmm3, xmm1 .LBB0_7: vmovapd xmm0, xmm1 ret
, , . , , . ~10% .
. .
int64_t sum(int64_t x, int64_t y) { return x + y; } int64_t add1(int64_t x) { return sum(x, 1); } int64_t add2(int64_t x) { return sum(1, x); } int64_t add3(int64_t x) { return sum(-1, x) + 2; }
sum(long, long): # @sum(long, long) lea rax, [rdi + rsi] ret add1(long): # @add1(long) mov esi, 1 # . jmp sum(long, long) # TAILCALL add2(long): # @add2(long) mov rax, rdi # . mov edi, 1 mov rsi, rax jmp sum(long, long) # TAILCALL add3(long): # @add3(long) push rax # rax . mov rax, rdi # , add2, . mov rdi, -1 mov rsi, rax call sum(long, long) add rax, 2 # 2 . pop rcx ret
, , call
, jmp
. , , sum
, , add
.
, 10%.
:
. 2D :
struct Point { double x, y; }; struct ZeroPoint { double x{}, y{}; }; struct NanPoint { double x{quietNaN}, y{quietNaN}; };
Point
. ZeroPoint
. IEEE 754-1985:
The number zero is represented specially: sign = 0 for positive zero, 1 for negative zero; biased exponent = 0; fraction = 0;
memset
. NanPoint
numeric_limits<double>::quiet_NaN();
, .
Point data;
sub rsp, 24
- .
ZeroPoint data; Point data{};
.
sub rsp, 40 vxorps xmm0, xmm0, xmm0 vmovaps xmmword ptr [rsp + 16], xmm0
. xmm0
. XOR
vxorps
. .
NanPoint data;
sub rsp, 40 vmovaps xmm0, xmmword ptr [rip + .LCPI0_0]
, .
:
static constexpr size_t smallSize = 8; static constexpr size_t bigSize = 321; extern size_t smallUnknownSize;
.
array<Point, smallSize> data;
– .
sub rsp, 136
, .
array<ZeroPoint, smallSize> data; array<ZeroPoint, smallSize> data{}; array<Point, smallSize> data{};
.
sub rsp, 192 vxorps ymm0, ymm0, ymm0 vmovaps ymmword ptr [rsp + 128], ymm0 vmovaps ymmword ptr [rsp + 96], ymm0 vmovaps ymmword ptr [rsp + 64], ymm0 vmovaps ymmword ptr [rsp + 32], ymm0
256 , 2 .
array<NanPoint, smallSize> data; array<NanPoint, smallSize> data{};
sub rsp, 136 vmovaps xmm0, xmmword ptr [rip + .LCPI0_0]
.
array<Point, bigSize> data;
sub rsp, 5144
.
array<ZeroPoint, bigSize> data; array<ZeroPoint, bigSize> data{}; array<Point, bigSize> data{};
sub rsp, 5152 lea rbx, [rsp + 16] xor esi, esi mov edx, 5136 mov rdi, rbx call memset # memset(rsp+16, 0, 5136).
, memset
. , , rdi, esi, edx
. e
d
32 64- .
array<NanPoint, bigSize> data;
sub rsp, 5144 lea rax, [rsp + 8] lea rcx, [rsp + 5144] vmovaps xmm0, xmmword ptr [rip + .LCPI0_0]
. , 321 3 . rax, rcx
.
array<NanPoint, bigSize> data{};
sub rsp, 5152 lea rbx, [rsp + 16] xor esi, esi mov edx, 5136 mov rdi, rbx call memset # memset(rsp+16, 0, 5136). lea rax, [rsp + 5152] vmovaps xmm0, xmmword ptr [rip + .LCPI0_0] # xmm0 = [nan,nan] # . .LBB0_1: # =>This Inner Loop Header: Depth=1 vmovups xmmword ptr [rbx], xmm0 vmovups xmmword ptr [rbx + 16], xmm0 vmovups xmmword ptr [rbx + 32], xmm0 add rbx, 48 cmp rbx, rax jne .LBB0_1
. , memset
. , , . .
vector<Point> data(smallSize);
sub rsp, 40 vxorps xmm0, xmm0, xmm0 vmovaps xmmword ptr [rsp], xmm0 mov qword ptr [rsp + 16], 0 mov edi, 128 call operator new(unsigned long) # new(128). mov qword ptr [rsp], rax mov rcx, rax sub rcx, -128 mov qword ptr [rsp + 16], rcx vxorps xmm0, xmm0, xmm0 # . vmovups xmmword ptr [rax + 16], xmm0 vmovups xmmword ptr [rax], xmm0 vmovups xmmword ptr [rax + 32], xmm0 vmovups xmmword ptr [rax + 48], xmm0 vmovups xmmword ptr [rax + 64], xmm0 vmovups xmmword ptr [rax + 80], xmm0 vmovups xmmword ptr [rax + 96], xmm0 vmovups xmmword ptr [rax + 112], xmm0
new
. , T{}
, , . ZeroPoint
NanPoint
. :
vector<Point> data(bigSize);
main: # @main push rbx sub rsp, 48 vxorps xmm0, xmm0, xmm0 vmovaps xmmword ptr [rsp], xmm0 mov qword ptr [rsp + 16], 0 mov edi, 5136 call operator new(unsigned long) # new(5136). mov qword ptr [rsp], rax mov qword ptr [rsp + 8], rax mov rcx, rax add rcx, 5136 mov qword ptr [rsp + 16], rcx vxorps xmm0, xmm0, xmm0 vmovaps xmmword ptr [rsp + 32], xmm0 xor edx, edx # . .LBB0_2: # =>This Inner Loop Header: Depth=1 vmovaps xmm0, xmmword ptr [rsp + 32] vmovups xmmword ptr [rax + rdx], xmm0 vmovaps xmm0, xmmword ptr [rsp + 32] vmovups xmmword ptr [rax + rdx + 16], xmm0 vmovaps xmm0, xmmword ptr [rsp + 32] vmovups xmmword ptr [rax + rdx + 32], xmm0 add rdx, 48 cmp rdx, 5136 jne .LBB0_2 mov qword ptr [rsp + 8], rcx mov rax, rsp
, . NanPoint
.
vector<NanPoint> data(bigSize);
main: # @main push rbx sub rsp, 32 vxorps xmm0, xmm0, xmm0 vmovaps xmmword ptr [rsp], xmm0 mov qword ptr [rsp + 16], 0 mov edi, 5136 call operator new(unsigned long) # new(5136). mov qword ptr [rsp], rax mov qword ptr [rsp + 8], rax mov rcx, rax add rcx, 5136 mov qword ptr [rsp + 16], rcx xor edx, edx vmovaps xmm0, xmmword ptr [rip + .LCPI0_0] # xmm0 = [nan,nan] # NAN. .LBB0_2: # =>This Inner Loop Header: Depth=1 vmovups xmmword ptr [rax + rdx], xmm0 vmovups xmmword ptr [rax + rdx + 16], xmm0 vmovups xmmword ptr [rax + rdx + 32], xmm0 add rdx, 48 cmp rdx, 5136 jne .LBB0_2 mov qword ptr [rsp + 8], rcx mov rax, rsp
A ZeroPoint
.
vector<ZeroPoint> data(bigSize);
sub rsp, 32 vxorps xmm0, xmm0, xmm0 vmovaps xmmword ptr [rsp], xmm0 mov qword ptr [rsp + 16], 0 mov edi, 5136 call operator new(unsigned long) # new(5136). mov qword ptr [rsp], rax mov rbx, rax add rbx, 5136 mov qword ptr [rsp + 16], rbx xor esi, esi mov edx, 5136 mov rdi, rax call memset # memset(&data, 0, 5136).
memset
. , , memset
Point
. , , bigUnknownSize
.
vector<NanPoint> data(bigUnknownSize);
sub rsp, 32 mov rbx, qword ptr [rip + bigUnknownSize] vxorps xmm0, xmm0, xmm0 vmovaps xmmword ptr [rsp], xmm0 mov qword ptr [rsp + 16], 0 test rbx, rbx # bigUnknownSize == 0, new. je .LBB0_1 mov rax, rbx shr rax, 60 jne .LBB0_3 mov rdi, rbx shl rdi, 4 # 16 4. call operator new(unsigned long) # new(bigUnknownSize*16). jmp .LBB0_6 .LBB0_1: xor eax, eax .LBB0_6: mov rcx, rbx shl rcx, 4 add rcx, rax mov qword ptr [rsp], rax mov qword ptr [rsp + 8], rax mov qword ptr [rsp + 16], rcx test rbx, rbx # bigUnknownSize == 0, . je .LBB0_14 lea rdx, [rbx - 1] mov rsi, rbx and rsi, 7 je .LBB0_10 neg rsi vmovaps xmm0, xmmword ptr [rip + .LCPI0_0] # xmm0 = [nan,nan] # 0-7 . .LBB0_9: # =>This Inner Loop Header: Depth=1 vmovups xmmword ptr [rax], xmm0 dec rbx add rax, 16 inc rsi jne .LBB0_9 .LBB0_10: cmp rdx, 7 jb .LBB0_13 vmovaps xmm0, xmmword ptr [rip + .LCPI0_0] # xmm0 = [nan,nan] # 8. .LBB0_12: # =>This Inner Loop Header: Depth=1 vmovups xmmword ptr [rax], xmm0 vmovups xmmword ptr [rax + 16], xmm0 vmovups xmmword ptr [rax + 32], xmm0 vmovups xmmword ptr [rax + 48], xmm0 vmovups xmmword ptr [rax + 64], xmm0 vmovups xmmword ptr [rax + 80], xmm0 vmovups xmmword ptr [rax + 96], xmm0 vmovups xmmword ptr [rax + 112], xmm0 sub rax, -128 add rbx, -8 jne .LBB0_12 .LBB0_13: mov rax, rcx .LBB0_14: mov qword ptr [rsp + 8], rax mov rax, rsp
. , LBB0_9
, 8. 8 .
, Point
, ZeroPoint
memset
:
vector<ZeroPoint> data(bigUnknownSize);
sub rsp, 40 mov rbx, qword ptr [rip + bigUnknownSize] vxorps xmm0, xmm0, xmm0 vmovaps xmmword ptr [rsp], xmm0 mov qword ptr [rsp + 16], 0 test rbx, rbx # bigUnknownSize == 0, new. je .LBB0_1 mov rax, rbx shr rax, 60 jne .LBB0_3 mov rdi, rbx shl rdi, 4 call operator new(unsigned long) # new(bigUnknownSize*16). jmp .LBB0_6 .LBB0_1: xor eax, eax .LBB0_6: mov rdx, rbx shl rdx, 4 lea r14, [rax + rdx] mov qword ptr [rsp], rax mov qword ptr [rsp + 8], rax mov qword ptr [rsp + 16], r14 test rbx, rbx # bigUnknownSize == 0, memset. je .LBB0_8 xor esi, esi mov rdi, rax call memset # memset(&data, 0, bigUnknownSize*16). mov rax, r14 .LBB0_8: mov qword ptr [rsp + 8], rax mov rax, rsp
,
vector<NanPoint> data; data.resize(bigUnknownSize);
250 , . operator new
, .
.
Code | Cycles per iteration |
---|
Point p; | 4.5 |
ZeroPoint p; | 5.2 |
NanPoint p; | 4.5 |
array<Point, smallSize> p; | 4.5 |
array<ZeroPoint, smallSize> p; | 6.7 |
array<NanPoint, smallSize> p; | 6.7 |
array<Point, bigSize> p; | 4.5 |
array<ZeroPoint, bigSize> p; | 296.0 |
array<NanPoint, bigSize> p; | 391.0 |
array<Point, bigSize> p{}; | 292.0 |
array<NanPoint, bigSize> p{}; | 657.0 |
vector<Point> p(smallSize); | 32.3 |
vector<ZeroPoint> p(smallSize); | 33.8 |
vector<NanPoint> p(smallSize); | 33.8 |
vector<Point> p(bigSize); | 323.0 |
vector<ZeroPoint> p(bigSize); | 308.0 |
vector<NanPoint> p(bigSize); | 281.0 |
vector<ZeroPoint> p(smallUnknownSize); | 44.1 |
vector<NanPoint> p(smallUnknownSize); | 37.6 |
vector<Point> p(bigUnknownSize); | 311.0 |
vector<ZeroPoint> p(bigUnknownSize); | 315.0 |
vector<NanPoint> p(bigUnknownSize); | 290.0 |
vector<NanPoint> p; p.resize(bigUnknownSize); | 315.0 |
:
- .
- .
memset
, .- .
- , . .
- . .
- , . .