¿Por qué es importante romper la “dependencia de salida” de LZCNT?

Al hacer una evaluación comparativa de algo, medí un rendimiento mucho más bajo de lo que había calculado, que reduje a la instrucción LZCNT (también ocurre con TZCNT), como se demuestra en los siguientes puntos de referencia:

xor ecx, ecx _benchloop: lzcnt eax, edx add ecx, 1 jnz _benchloop 

Y:

  xor ecx, ecx _benchloop: xor eax, eax ; this shouldn't help, but it does lzcnt eax, edx add ecx, 1 jnz _benchloop 

La segunda versión es mucho más rápida. No debería ser. No hay ninguna razón por la cual LZCNT debería tener una dependencia de entrada en su salida. A diferencia de BSR / BSF, las instrucciones xZCNT siempre sobrescriben su salida.

Estoy ejecutando esto en un 4770K, por lo que LZCNT y TZCNT no se están ejecutando como BSR / BSF.

¿Que está pasando aqui?

Esto es simplemente una limitación en la microarchitecture de su CPU Intel Haswell y varias CPUs previas. Se ha corregido para tzcnt y lzcnt partir de Skylake, pero el problema persiste para popcnt .

En esas tzcnt , el operando de destino para tzcnt , lzcnt y popcnt se trata como una dependencia de entrada aunque, semánticamente, no lo es. Ahora dudo que esto sea realmente un “error”: si se tratara simplemente de un descuido, espero que se haya solucionado en una de las varias microarchitectures nuevas que se han lanzado desde que se introdujo.

Lo más probable es que se trate de un compromiso de diseño basado en uno o ambos de los siguientes dos factores:

  • Es probable que el hardware para popcnt , lzcnt y tzcnt se comparta con las instrucciones bsf y bsr existentes. Ahora bsf y bsr tenían una dependencia del valor de destino anterior en la práctica 2 para el caso especial de entrada de todos los bits cero, ya que los chips Intel dejaron el destino sin modificar en ese caso. Por lo tanto, es muy posible que el diseño más simple para el hardware combinado resulte en que las otras instrucciones similares se ejecuten en la misma unidad heredando la misma dependencia.

  • La gran mayoría de las instrucciones ALU de dos operandos x86 tienen una dependencia en el operando de destino, ya que también se utiliza como fuente. Las tres instrucciones afectadas son algo únicas en el sentido de que son operadores unarios , pero a diferencia de operadores unarios existentes como not y neg que tienen un solo operando utilizado como fuente y destino, tienen operandos de origen y destino distintos, haciéndolos superficialmente similares a la mayoría 2- instrucciones de entrada. Tal vez los circuitos renamer / scheduler simplemente no distinguen el caso especial de estos operandos unarios-con-dos-registros frente a la gran mayoría de las instrucciones simples de fuente-destino de 2 entradas que no tienen esta dependencia.

De hecho, para el caso del popcnt Intel ha emitido varias erratas que cubren el problema de falsa dependencia, como HSD146 para Haswell Desktop y SKL029 para Skylake , que dice:

La instrucción POPCNT puede tardar más en ejecutarse de lo esperado

Problema La ejecución de la instrucción POPCNT con un operando de 32 o 64 bits puede retrasarse hasta que se ejecuten las instrucciones previas no dependientes.

El software de implicación que utiliza la instrucción POPCNT puede experimentar un rendimiento inferior al esperado.

Solución alternativa Ninguna identificada

Siempre encontré esta errata inusual ya que en realidad no identifica ningún tipo de defecto funcional o no conformidad con la especificación, que es el caso para esencialmente todas las otras erratas. Intel realmente no documenta un modelo de rendimiento específico para el motor de ejecución OoO y hay un montón de otros “errores” de rendimiento que han aparecido y desaparecido a lo largo de los años (muchos con un impacto mucho mayor que este problema menor) que no hacen ser documentado en errata Aún así, esto tal vez proporciona alguna evidencia de que puede considerarse un error. Curiosamente, la errata nunca se extendió para incluir tzcnt o lzcnt que tenían el mismo problema cuando se introdujeron.


1 Well tzcnt y lzcnt solo aparecieron en Haswell, pero también existe el problema del popcnt que se introdujo en Nehalem, pero el problema de la falsa dependencia quizás solo exista para Sandy Bridge o más tarde.

2 En la práctica , aunque no está documentado en los documentos de ISA, dado que el resultado para la entrada de cero fue indefinido en los manuales de Intel. Sin embargo, la mayoría o todos los chips Intel implementaron el comportamiento sin cambiar el registro de destino en este caso.

En la línea de lo que sugirió @BrettHale, es posible (si es extraño) que estés golpeando un puesto de actualización de banderas parciales en un rincón. El estado del indicador debería, en teoría, ser simplemente renombrado porque los siguientes agregan actualizaciones a todos los indicadores, pero si no es por alguna razón, entonces introduciría una dependencia transportada por bucle, e insertar xor rompería esa dependencia.

Es difícil saber con certeza si esto es lo que está sucediendo, pero parece ser una explicación casual la explicación más probable; puede probar la hipótesis reemplazando el xor con la test (que también rompe la dependencia de indicadores pero no tiene efecto en las dependencias de registro).