Wie man nicht die schnellste Kraft macht und einen Fehler in der Visual Studio 2019 Community findet

Ich wurde von einem Artikel über die Verwendung der "seltsamen" Popcount-Anweisung in modernen Prozessoren aufgefordert. Hier geht es nicht darum, die Anzahl der Einheiten zu zählen, sondern das Vorzeichen des Endes der C-Linie ( nullterminierte Linie ) zu erkennen.
Eine nullterminierte Zeichenfolge ist eine Methode zur Darstellung von Zeichenfolgen in Programmiersprachen, bei der anstelle eines speziellen Zeichenfolgentyps ein Array von Zeichen verwendet wird und das Ende der Zeichenfolge das erste spezielle Nullzeichen ist (NUL aus ASCII-Code mit dem Wert 0).

Um die Länge eines solchen Terms zu bestimmen, wird eine Standardfunktion angewendet.

size_t __cdecl strlen(char const* str) 

Der Operationsalgorithmus kann in C beschrieben werden als:

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

Mal sehen, was der Community-Compiler von MS Visual Studio 2019 (Release, x86) daraus macht:

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

Das heißt, ein Byte wird aus dem Speicher geladen und mit Null verglichen. Strlen-Aufrufe werden durch denselben Code ersetzt, wenn Sie das Projekt in Release erstellen. Der Algorithmus ist korrekt, aber die Geschwindigkeit scheint mir nicht ausreichend zu sein. Was passiert, wenn Sie den Code mit einem Aufruf von standard strlen in Debug kompilieren? - Die Funktion strlen library wird wie erwartet aufgerufen, aber von einer Person manuell auf Assembler geschrieben.

Spoiler für strlen Standardcode in 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 


Tabelle 1 - Laufzeit des Strlen-Benchmarks in Sekunden (MS VS 2019-Community, C ++ cl-Version: 19.22.27905)

Großer Block, 1KBig Block, 1K, * strlen callKleiner Block, 10 ElementeKleiner Block, 10 Elemente, * strlen call
Debug, x867.257.253,063,06
Release, x869.03.90,150,12
Debug, x646.06.03.43.4
Release, x648.52.30,150,11

* Zwingen Sie den Compiler, die Funktion strlen library aufzurufen

Wir können daher den Schluss ziehen, dass die Substitution des Byte-Vergleichs durch den MS-Compiler selbst bei kleinen Zeichenfolgen ineffizient ist, und bei großen Zeichenfolgen liegt Debug vor Release!

Bankcode
 #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; } 


String
 len_result += strlen(str_for_test); 
kompiliert
Debug in: Aufruf der Funktion strlen library;
Release in: Byte-Vergleich.

Wenn Sie es kommentieren und schreiben

 len_result += strlen_func(str_for_test); 

wo

 auto strlen_func = strlen; 

Wir werden den Compiler zwingen, immer die Bibliotheksfunktion aufzurufen.

Aufgrund dessen wurde die Beschleunigung der Bibliotheksfunktion vor dem Byte-Vergleich um das 2,3-fache erreicht (für Release x86, 1k)?

Aufgrund des Vergleichs nicht um ein Byte, sondern sofort um 4. Die ganze Magie hier:

 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 

Ist es möglich, mit Vektoranweisungen moderner Prozessoren schneller zu arbeiten? Lass es uns versuchen.

Unter Verwendung des Intel Intrinsics-Handbuchs finden wir das intrinsische _mm_cmpistri SSE4.2, das nur für die Arbeit mit Zeichenfolgen entwickelt wurde. Dem Eingang werden zwei Vektoren mit einer Länge von 128 Bit und eine Operationsmaske zugeführt. Als Maske verwenden wir: _SIDD_UBYTE_OPS = 0 - Datentyp, _SIDD_CMP_EQUAL_EACH = 8 - Operation der Byte-Ausrichtung, und wir werden mit einem Nullvektor vergleichen. Der zurückgegebene Wert ist die Anzahl der ersten paarweise ungleichen Elemente (dh wenn das Element von links nach rechts übereinstimmt, stoppt die Zählung, ich bin froh, wenn jemand das Verhalten bestätigt).

 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; 

Code

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

Dient zum Ausgleich der Adresse der geladenen Leitung. Die Adresse wird in Vielfachen von 16 benötigt, damit die meisten SSE-Anweisungen funktionieren. Für die von uns verwendete pcmpistri-Anweisung ist eine Ausrichtung unbedingt nicht erforderlich, eine Zugriffsausnahme wird nicht ausgelöst.
Eigen
 __m128i data = _mm_load_si128((__m128i*)(str + length)); if ((res = _mm_cmpistri(z128, data, _SIDD_CMP_EQUAL_EACH)) != 16) 

Zusammengestellt in:
 pcmpistri xmm1,xmmword ptr [eax+edx],8 

Das Ausrichten auf 16 ist jedoch auch in unserem Fall nützlich, da es die Geschwindigkeit geringfügig erhöht. Daher sind wir sicher, dass ein Lesezyklus von 16 Byte nicht zu einer möglicherweise nicht zugewiesenen Seite ( 4K-Speicherseite ) führt.

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

"Ruft" die Größe des Strings ab, wenn sein Ende erkannt wird (da ich mir über den Operationsalgorithmus _mm_cmpistri nicht ganz sicher bin).
gelöscht nach picul Kommentar, der eine Erhöhung auf kleinen Zeilen gab.

Haben wir die schnellste Strecke gemacht? - Leider nein, die Jungs von https://www.strchr.com/sse2_optimised_strlen haben es noch schneller und ohne SSE4.2 gemacht.

Tabelle 2 - Laufzeit des Strlen-Benchmarks in Sekunden (Release)
Anzahl der ZeichenMS-Byte-VergleichFrau 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

Schlussfolgerungen:

Es scheint mir, dass MS immer Library Strlen aufrufen muss und keine Substitutionsbyte-Vergleiche durchführen muss.

UPD
X64-Test hinzugefügt.
Letzte Schleife in strlen_SSE4 entfernt

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


All Articles