2011-04-04 24 views
6

Actualmente estoy experimentando con la creación de funciones altamente optimizadas y reutilizables para una biblioteca mía. Por ejemplo, escribo la función "es potencia de 2" de la siguiente manera:¿Es posible la función "intrínseca personalizada" para x64 en lugar del montaje en línea?

template<class IntType> 
inline bool is_power_of_two(const IntType x) 
{ 
    return (x != 0) && ((x & (x - 1)) == 0); 
} 

Este es un portátil, la implementación de bajo mantenimiento como una plantilla de línea C++. Este código se compila por VC++ 2008 para el siguiente código con ramas:

is_power_of_two PROC 
    test rcx, rcx 
    je SHORT [email protected]_power_o 
    lea rax, QWORD PTR [rcx-1] 
    test rax, rcx 
    jne SHORT [email protected]_power_o 
    mov al, 1 
    ret 0 
[email protected]_power_o: 
    xor al, al 
    ret 0 
is_power_of_two ENDP 

He encontrado también la implementación de aquí: "The bit twiddler", que se codifica en el montaje para x64 de la siguiente manera:

is_power_of_two_fast PROC 
    test rcx, rcx 
    je SHORT NotAPowerOfTwo 
    lea rax, [rcx-1] 
    and rax, rcx 
    neg rax 
    sbb rax, rax 
    inc rax 
    ret 
NotAPowerOfTwo: 
    xor rax, rax 
    ret 
is_power_of_two_fast ENDP 

I probado ambas subrutinas escritas separadamente de C++ en un módulo de ensamblaje (archivo .asm), ¡y el segundo funciona aproximadamente un 20% más rápido!

Sin embargo, la sobrecarga de la llamada de función es considerable: si comparo la implementación del segundo conjunto "is_power_of_two_fast" con la versión en línea de la función de plantilla, ¡esta última es más rápida a pesar de las ramas!

Desafortunadamente, las nuevas convenciones para x64 especifican que no está permitido el ensamblaje en línea. Uno debería usar "funciones intrínsecas".

Ahora la pregunta: ¿puedo implementar la versión más rápida "is_power_of_two_fast" como una función intrínseca personalizada o algo similar, para que pueda usarse en línea? O, alternativamente, ¿es posible obligar de algún modo al compilador a producir la versión de baja ramificación de la función?

+0

GCC y ICC todavía permitir el ensamblaje en línea – hirschhornsalz

+0

Evitar la rama mediante el uso de y en lugar de &&. –

+0

@drhirsch: gracias, lo recuerdo. @ Hans Passant: Ya lo intenté, pero me lleva a un código más lento (demasiadas instrucciones). –

Respuesta

2

Incluso VC 2005 es capaz de producir código con instrucción sbb.

para el código C

bool __declspec(noinline) IsPowOf2(unsigned int a) 
{ 
    return (a>=1)&((a&(a-1))<1); 
} 

compila a la siguiente

00401000 lea   eax,[ecx-1] 
00401003 and   eax,ecx 
00401005 cmp   eax,1 
00401008 sbb   eax,eax 
0040100A neg   eax 
0040100C cmp   ecx,1 
0040100F sbb   ecx,ecx 
00401011 add   ecx,1 
00401014 and   eax,ecx 
00401016 ret   
0

La única manera de avanzar es retroceder un poco y comenzar a mirar la imagen más grande. Deje de implementar la API micro optimizada o progrese para hacer llamadas API más grandes, todas optimizadas en MASM64, YASM, NASM, etc.

Si usa uno de los ensambladores más potentes, puede convertir las funciones pequeñas en macros, así que básicamente cambie su función de ensamblador en línea basada en el encabezado C/C++ en un ensamblador incluye un archivo.

2

No, no puede implementar ningún intrínseco personalizado, todos están integrados en el compilador. No solo están integradas las instrucciones, sino que el compilador también conoce la semántica de lo intrínseco y adapta el código para diferentes códigos circundantes.

Una razón por la que se elimina el ensamblaje en línea para x86-64 es que insertar el ensamblaje en el medio de una función perturba el optimizador y, a menudo, produce un código menos optimizado alrededor del código del ensamblador. ¡Fácilmente puede haber una pérdida neta allí!

El único uso real para intrínsecos es para instrucciones especiales "interesantes" que el compilador no puede generar a partir de construcciones C o C++, como BSF o BSR. La mayoría de las demás cosas funcionarán mejor con las funciones en línea, como la plantilla anterior.

Si necesita hacer algo especial, que el compilador no entiende, la única opción real es escribir la función completa como un módulo de ensamblador separado. Si la sobrecarga de llamadas para esa función es demasiado costosa, la optimización probablemente no valió tanto en primer lugar.

¡Confíe en su compilador (tm)!

1

VC10 x64 intrinsics no sería de gran ayuda en este caso simple. La bifurcación dinámica que tiene se debe al operador & & que es un operador de salida anticipada. En muchos casos (su caso es un ejemplo perfecto) es mejor evitar la bifurcación calculando el resultado para todas las ramas y luego aplicar una máscara para seleccionar la buena. Un código de CPP con enmascaramiento se vería así:

template<typename T_Type> 
inline bool isPowerOfTwo(T_Type const& x) 
{ 
    // static type checking for the example 
    static_assert(std::is_integral<T_Type>::value && std::is_unsigned<T_Type>::value, "limited to unsigned types for the example"); 
    typedef std::make_signed<T_Type>::type s_Type; 

    // same as yours but with no branching 
    return bool( ((s_Type(s_Type(x != 0) << (s_Type(sizeof(T_Type)<<3u)-1))) >> (s_Type(s_Type(sizeof(T_Type)<<3u)-1))) & ((x & (x - 1)) == 0) ); 
} 

En el código anterior no estoy comprobando si el número es negativo o no para este tipo firmados. De nuevo, una máscara simple hará el truco realizando un desplazamiento aritmético a la derecha (numBit-1) veces para obtener un valor de (~ 0) para los números negativos y 0 para los positivos

+0

Desafortunadamente, lo que sugiere no es muy diferente de la función inicial de C++. Una compilación con salida de conjunto revela que VC++ 2008 usa la instrucción "prueba" al compilar su código, y las ramas todavía están allí. –

Cuestiones relacionadas