C贸mo no hacer el m谩s r谩pido y encontrar un defecto en la comunidad de Visual Studio 2019

Me impuls贸 un art铆culo sobre el uso de la instrucci贸n "popcount" extra帽a en los procesadores modernos . No se trata de contar el n煤mero de unidades, sino de detectar el signo del final de la l铆nea C ( l铆nea terminada en cero ).
Una cadena terminada en nulo es una forma de representar cadenas en lenguajes de programaci贸n, en la que, en lugar de introducir un tipo de cadena especial, se utiliza una matriz de caracteres, y el final de la cadena es el primer car谩cter nulo especial encontrado (NUL del c贸digo ASCII, con un valor de 0).

Para determinar la duraci贸n de dicho t茅rmino, se aplica una funci贸n est谩ndar.

size_t __cdecl strlen(char const* str) 

El algoritmo de operaci贸n del cual se puede describir en C como:

 size_t strlen_algo(const char* str) { size_t length = 0; while (*str++) length++; return length; } 

Veamos en qu茅 se convierte el compilador de la comunidad de MS Visual Studio 2019 (Release, x86):

 08811F7h: mov al,byte ptr [ecx] inc ecx test al,al jne main+0D7h (08811F7h) 

Es decir, un byte se carga desde la memoria y se compara con cero. El mismo c贸digo se sustituye por las llamadas eliminadas si construye el proyecto en Release, el algoritmo es correcto, pero la velocidad, me parece, no es suficiente. 驴Qu茅 sucede si compila el c贸digo con una llamada a strlen est谩ndar en Debug? - Se llamar谩 a la funci贸n de biblioteca strlen, como se esperaba, pero una persona la escribir谩 manualmente en el ensamblador.

Spoiler de c贸digo de la funci贸n strlen est谩ndar en MS Visual Studio
 ;strlen.asm - contains strlen() routine ; ; Copyright (c) Microsoft Corporation. All rights reserved. ; ;Purpose: ; strlen returns the length of a null-terminated string, ; not including the null byte itself. ; ;******************************************************************************* .xlist include cruntime.inc .list page ;*** ;strlen - return the length of a null-terminated string ; ;Purpose: ; Finds the length in bytes of the given string, not including ; the final null character. ; ; Algorithm: ; int strlen (const char * str) ; { ; int length = 0; ; ; while( *str++ ) ; ++length; ; ; return( length ); ; } ; ;Entry: ; const char * str - string whose length is to be computed ; ;Exit: ; EAX = length of the string "str", exclusive of the final null byte ; ;Uses: ; EAX, ECX, EDX ; ;Exceptions: ; ;******************************************************************************* CODESEG public strlen strlen proc \ buf:ptr byte OPTION PROLOGUE:NONE, EPILOGUE:NONE .FPO ( 0, 1, 0, 0, 0, 0 ) string equ [esp + 4] mov ecx,string ; ecx -> string test ecx,3 ; test if string is aligned on 32 bits je short main_loop str_misaligned: ; simple byte loop until string is aligned mov al,byte ptr [ecx] add ecx,1 test al,al je short byte_3 test ecx,3 jne short str_misaligned add eax,dword ptr 0 ; 5 byte nop to align label below align 16 ; should be redundant main_loop: mov eax,dword ptr [ecx] ; read 4 bytes mov edx,7efefeffh add edx,eax xor eax,-1 xor eax,edx add ecx,4 test eax,81010100h je short main_loop ; found zero byte in the loop mov eax,[ecx - 4] test al,al ; is it byte 0 je short byte_0 test ah,ah ; is it byte 1 je short byte_1 test eax,00ff0000h ; is it byte 2 je short byte_2 test eax,0ff000000h ; is it byte 3 je short byte_3 jmp short main_loop ; taken if bits 24-30 are clear and bit ; 31 is set byte_3: lea eax,[ecx - 1] mov ecx,string sub eax,ecx ret byte_2: lea eax,[ecx - 2] mov ecx,string sub eax,ecx ret byte_1: lea eax,[ecx - 3] mov ecx,string sub eax,ecx ret byte_0: lea eax,[ecx - 4] mov ecx,string sub eax,ecx ret strlen endp end 


Tabla 1: tiempo de ejecuci贸n del benchmark strlen en segundos (comunidad MS VS 2019, versi贸n C ++ cl: 19.22.27905)

Bloque grande, 1KBloque grande, 1K, * llamada strlenBloque peque帽o, 10 elementos.Bloque peque帽o, 10 elementos, * llamada strlen
Depuraci贸n, x867.257.253,063,06
Lanzamiento, x869.03.90,150,12
Depuraci贸n, x646.06.03.43.4
Lanzamiento, x648.52.30,150,11

* obligar al compilador a llamar a la funci贸n de biblioteca strlen

Por lo tanto, podemos concluir que la sustituci贸n de la comparaci贸n de bytes por el compilador MS VS es ineficiente incluso en cadenas peque帽as, y en cadenas grandes, 隆Debug est谩 por delante de Release!

C贸digo de banco
 #include <iostream> #include <string> #include <vector> #include <chrono> #include <nmmintrin.h> inline size_t strlen_algo(const char* str) { size_t length = 0; while (*str++) length++; return length; } inline size_t strlen_sse4(const char* str) { size_t length = 0; int res = 0; //align to 16 bytes while (((size_t(str+length)) & (sizeof(__m128i) - 1)) != 0) { if (str[length] == 0) return length; length++; } __m128i z128 = _mm_setzero_si128(); for(; ; length += 16) { __m128i data = _mm_load_si128((__m128i*)(str + length)); if ((res = _mm_cmpistri(z128, data, _SIDD_CMP_EQUAL_EACH)) != 16) break; } /*while (str[length]) length++;*/ return length + res; } #define _DISABLE_ASM_BSF //https://www.strchr.com/sse2_optimised_strlen #ifndef WORDS_BIGENDIAN #if 0 #elif 0 #else static inline int count_bits_to_0(unsigned int x) // counting trailing zeroes, by Nazo, post: 2009/07/20 03:40 { // this is current winner for speed static const unsigned char table[256] = { 7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, }; if ((unsigned char)x) return table[(unsigned char)x]; return table[x >> 8] + 8; // t[x / 256] + 8 } #endif #else #if 0 static inline int count_bits_to_0(unsigned int x) // counting trailing zeroes { register int i = 0; if (!(x & (1 << 15))) i++; else return i; if (!(x & (1 << 14))) i++; else return i; if (!(x & (1 << 13))) i++; else return i; if (!(x & (1 << 12))) i++; else return i; if (!(x & (1 << 11))) i++; else return i; if (!(x & (1 << 10))) i++; else return i; if (!(x & (1 << 9))) i++; else return i; if (!(x & (1 << 8))) i++; else return i; if (!(x & (1 << 7))) i++; else return i; if (!(x & (1 << 6))) i++; else return i; if (!(x & (1 << 5))) i++; else return i; if (!(x & (1 << 4))) i++; else return i; if (!(x & (1 << 3))) i++; else return i; if (!(x & (1 << 2))) i++; else return i; if (!(x & (1 << 1))) i++; else return i; if (!(x & (1 << 0))) i++; return i; } #else static inline int count_bits_to_0(unsigned int x) // counting trailing zeroes { // http://www.hackersdelight.org/: nlz1() shortened for 16-bit mask register int n = 0; if (x <= 0x000000FFU) { n = n + 8; x = x << 8; } if (x <= 0x00000FFFU) { n = n + 4; x = x << 4; } if (x <= 0x00003FFFU) { n = n + 2; x = x << 2; } if (x <= 0x00007FFFU) { n = n + 1; } return n; } #endif #endif size_t strlen2(const char* str) { register size_t len = 0; // align to 16 bytes while ((((intptr_t)str) & (sizeof(__m128i) - 1)) != 0) { if (*str++ == 0) return len; ++len; } // search for 0 __m128i xmm0 = _mm_setzero_si128(); __m128i xmm1; int mask = 0; for (;;) { xmm1 = _mm_load_si128((__m128i*)str); xmm1 = _mm_cmpeq_epi8(xmm1, xmm0); if ((mask = _mm_movemask_epi8(xmm1)) != 0) { // got 0 somewhere within 16 bytes in xmm1, or within 16 bits in mask // find index of first set bit #ifndef _DISABLE_ASM_BSF // define it to disable ASM #if (_MSC_VER >= 1300) // make sure <intrin.h> is included unsigned long pos; _BitScanForward(&pos, mask); len += (size_t)pos; #elif defined(_MSC_VER) // earlier MSVC's do not have _BitScanForward, use inline asm __asm bsf edx, mask; edx = bsf(mask) __asm add edx, len; edx += len __asm mov len, edx; len = edx #elif ((__GNUC__ >= 4) || ((__GNUC__ == 3) && (__GNUC_MINOR__ >= 4))) // modern GCC has built-in __builtin_ctz len += __builtin_ctz(mask); #elif defined(__GNUC__) // older GCC shall use inline asm unsigned int pos; asm("bsf %1, %0" : "=r" (pos) : "rm" (mask)); len += (size_t)pos; #else // none of choices exist, use local BSF implementation len += count_bits_to_0(mask); #endif #else len += count_bits_to_0(mask); #endif break; } str += sizeof(__m128i); len += sizeof(__m128i); } return len; } int main() { std::vector<std::string> vstr; const int str_num = 1024; const int str_size = 1024; size_t len_result = 0; srand(0); for (int i = 0; i < str_num; i++) { std::string str1; for (int j = 0; j < str_size; j++) { str1.push_back('0' + rand() % 78); } vstr.push_back(std::move(str1)); } auto strlen_func = strlen; //auto strlen_func = strlen_algo; //auto strlen_func = strlen_sse4; //auto strlen_func = strlen2; auto time_std = std::chrono::steady_clock::now(); for (int k = 0; k < 10*1000; k++) { for (int i = 0; i < str_num; i++) { const char* str_for_test = vstr[i].c_str(); len_result += strlen_func(str_for_test); //len_result += strlen(str_for_test); } for (int i = 0; i < str_num; i++) { const char* str_for_test = vstr[i].c_str(); len_result -= strlen_func(str_for_test); //len_result -= strlen(str_for_test); } } auto finish = std::chrono::steady_clock::now(); double elapsed_seconds = std::chrono::duration_cast<std::chrono::duration<double>>(finish - time_std).count(); std::cout << "\n" << len_result; std::cout << "\n\nTime: " << elapsed_seconds; return 0; } 


Cadena
 len_result += strlen(str_for_test); 
compila
Depurar en: llamar a la funci贸n de biblioteca strlen;
Lanzamiento en: comparaci贸n de bytes.

Si comentas y escribes

 len_result += strlen_func(str_for_test); 

donde

 auto strlen_func = strlen; 

obligaremos al compilador a llamar siempre a la funci贸n de biblioteca.

驴Debido a lo que se logr贸 la aceleraci贸n de la funci贸n de biblioteca antes de la comparaci贸n de bytes en 2,3 veces (para Release, x86, 1k)?

Debido a la comparaci贸n, no por un byte, sino inmediatamente por 4. Toda la magia aqu铆:

 main_loop: mov eax,dword ptr [ecx] ; read 4 bytes mov edx,7efefeffh add edx,eax xor eax,-1 xor eax,edx add ecx,4 test eax,81010100h je short main_loop 

驴Es posible hacerlo m谩s r谩pido usando instrucciones vectoriales de procesadores modernos? Prob茅moslo.

Usando la gu铆a Intel Intrinsics , encontramos el _mm_cmpistri SSE4.2 intr铆nseco, dise帽ado solo para trabajar con cadenas. Dos vectores de longitud de 128 bits y una m谩scara de operaciones se alimentan a la entrada. Como m谩scara usamos: _SIDD_UBYTE_OPS = 0 - tipo de datos, _SIDD_CMP_EQUAL_EACH = 8 - operaci贸n de alineaci贸n de bytes, y la compararemos con un vector cero. El valor devuelto ser谩 el n煤mero de los primeros elementos desiguales por pares (es decir, si el elemento coincide de izquierda a derecha, el recuento se detiene, me alegrar谩 si alguien confirma el comportamiento).

 size_t length = 0; int res = 0; //align to 16 bytes while (((size_t(str+length)) & (sizeof(__m128i) - 1)) != 0) { if (str[length] == 0) return length; length++; } __m128i z128 = _mm_setzero_si128(); for(; ; length += 16) { __m128i data = _mm_load_si128((__m128i*)(str + length)); if ((res = _mm_cmpistri(z128, data, _SIDD_CMP_EQUAL_EACH)) != 16) break; } /*while (str[length]) length++;*/ return length + res; 

C贸digo

 while (((size_t(str+length)) & (sizeof(__m128i) - 1)) != 0) { if (str[length] == 0) return length; length++; } 

Sirve para igualar la direcci贸n de la l铆nea cargada, la direcci贸n se necesita en m煤ltiplos de 16 para que la mayor铆a de las instrucciones SSE funcionen. Para la instrucci贸n pcmpistri utilizada por nosotros, la alineaci贸n no es estrictamente necesaria, no se lanzar谩 una excepci贸n de acceso.
Intr铆nseca
 __m128i data = _mm_load_si128((__m128i*)(str + length)); if ((res = _mm_cmpistri(z128, data, _SIDD_CMP_EQUAL_EACH)) != 16) 

Compilado en:
 pcmpistri xmm1,xmmword ptr [eax+edx],8 

Sin embargo, la alineaci贸n a 16 tambi茅n es 煤til en nuestro caso, ya que proporciona un peque帽o aumento en la velocidad, por lo que estamos seguros de que un ciclo de lectura de 16 bytes no ir谩 a una p谩gina potencialmente no asignada ( p谩gina de memoria 4K ).

Ciclo:
 while (str[length]) length++; 

"Obtiene" el tama帽o de la cadena si se detecta su final (ya que no estoy completamente seguro del algoritmo de operaci贸n _mm_cmpistri).
eliminado despu茅s del comentario picul , lo que dio un aumento en las l铆neas peque帽as.

驴Hicimos el strlen m谩s r谩pido? - Desafortunadamente, no, los chicos de https://www.strchr.com/sse2_optimised_strlen lo hicieron a煤n m谩s r谩pido y sin usar SSE4.2.

Tabla 2: tiempo de ejecuci贸n del benchmark strlen en segundos (lanzamiento)
Numero de caracteresComparaci贸n de bytes MSMs strlenSSE 4.2SSE 2
10, x860,150,120,120,125
1K, x869.03.91,651,42
10, x640,150,110,080.1
1K, x648.52.31.61,32

Conclusiones:

Me parece que MS siempre necesita llamar a la biblioteca strlen, y no hacer comparaciones de bytes de sustituci贸n.

UPD
Se agreg贸 prueba x64.
Se elimin贸 el 煤ltimo bucle en strlen_SSE4

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


All Articles