assembly - ¿Las instrucciones de RMW se consideran perjudiciales en el x86 moderno?
optimization intel (1)
¿Las instrucciones de RMW se consideran perjudiciales en el x86 moderno?
No.
En las modernas x86 / x64, las instrucciones de entrada se traducen a uops.
Cualquier instrucción de RMW se dividirá en varios uops; de hecho, en el mismo uops en el que se dividirían las instrucciones por separado.
Al usar una instrucción RMW "compleja" en lugar de instrucciones de lectura, modificación y escritura separadas y "simples", obtienes lo siguiente.
- Menos instrucciones para decodificar.
- Mejor utilización de la memoria caché de instrucciones
- Mejor utilización de los registros direccionables
Puedes ver esto bastante claramente en las tablas de instrucciones de Agner Fog .
ADD [mem],const
tiene una latencia de 5 ciclos.
MOV [mem],reg
y visa versa tiene una latencia de 2 ciclos cada uno y un ADD reg,const
tiene una latencia de 1 para un total de 5.
Revisé los tiempos para Intel Skylake, pero AMD K10 es el mismo.
Debe tener en cuenta que los compiladores tienen que abastecer a muchos procesadores diferentes y algunos compiladores incluso usan la misma lógica de núcleo para diferentes familias de procesadores. Esto puede conducir a estrategias bastante subóptimas.
Dirección relativa de RIP
En X64, el direccionamiento relativo de RIP toma un ciclo extra para resolver el RIP en procesadores anteriores.
Skylake no tiene este retraso y estoy seguro de que otros eliminarán el retraso también.
Estoy seguro de que sabe que x86 no admite el direccionamiento relativo de EIP; en X86 tienes que hacer esto de forma redonda.
Recuerdo que las instrucciones de lectura, modificación y escritura generalmente se deben evitar al optimizar x86 para la velocidad. Es decir, debe evitar algo como add [rsi], 10
, que se agrega a la ubicación de la memoria almacenada en rsi
. La recomendación generalmente era dividirla en una instrucción de lectura y modificación, seguida de una tienda, por lo que algo así como:
mov rax, 10
add rax, [rsp]
mov [rsp], rax
Alternativamente, puede usar carga y almacenamientos explícitos y una operación de agregar reg-reg:
mov rax, [esp]
add rax, 10
mov [rsp], rax
¿Sigue siendo un consejo razonable (y lo fue alguna vez?) Para el moderno x86. 1
Por supuesto, en los casos en que un valor de la memoria se usa más de una vez, RMW es inapropiado, ya que incurrirá en cargas y tiendas redundantes. Me interesa el caso en el que un valor solo se usa una vez.
Basado en la exploración en Godbolt, todos los icc, clang y gcc prefieren usar una única instrucción RMW para compilar algo como:
void Foo::f() {
x += 10;
}
dentro:
Foo::f():
add QWORD PTR [rdi], 10
ret
Entonces, al menos, la mayoría de los compiladores parecen pensar que RMW está bien, cuando el valor solo se usa una vez.
Curiosamente, los diversos compiladores no están de acuerdo cuando el valor incrementado es global, en lugar de miembro, como por ejemplo:
int global;
void g() {
global += 10;
}
En este caso, gcc
y clang
siguen siendo una única instrucción RMW, mientras que icc
prefiere un agregado reg-reg con cargas y tiendas explícitas:
g():
mov eax, DWORD PTR global[rip] #5.3
add eax, 10 #5.3
mov DWORD PTR global[rip], eax #5.3
ret
Quizás sea algo relacionado con el direccionamiento relativo de RIP
y las limitaciones de micro fusión. Sin embargo, icc13 sigue haciendo lo mismo con -m32
por lo que quizás tenga que ver más con el modo de direccionamiento que requiere un desplazamiento de 32 bits.
1 Estoy usando el término deliberadamente vago moderno x86 para referirme básicamente a las últimas generaciones de chips para computadoras portátiles / desktop / server de Intel y AMD.