Bagaimana tidak, membuat strlen tercepat dan menemukan kekurangan di Visual Studio 2019 Community

Saya diminta oleh sebuah artikel tentang menggunakan instruksi popcount "aneh" dalam prosesor modern . Ini bukan tentang menghitung jumlah unit, tetapi tentang mendeteksi tanda akhir garis C (garis putus-nol ).
String null-terminated adalah cara untuk mewakili string dalam bahasa pemrograman, di mana alih-alih memperkenalkan tipe string khusus, array karakter digunakan, dan ujung string adalah karakter null khusus pertama yang ditemui (NUL dari kode ASCII, dengan nilai 0).

Untuk menentukan panjang istilah seperti itu, fungsi standar diterapkan.

size_t __cdecl strlen(char const* str) 

Algoritma operasi yang dapat dijelaskan dalam C sebagai:

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

Mari kita lihat apa yang kompiler komunitas MS Visual Studio 2019 (Rilis, x86) mengubahnya menjadi:

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

Artinya, satu byte dimuat dari memori dan dibandingkan dengan nol. Kode yang sama diganti untuk panggilan strlen jika Anda membangun proyek di Release, algoritmenya benar, tetapi kecepatannya, menurut saya, tidak cukup. Apa yang terjadi jika Anda mengkompilasi kode dengan panggilan ke strlen standar di Debug? - Fungsi pustaka strlen akan dipanggil, seperti yang diharapkan, tetapi ditulis oleh seseorang secara manual pada assembler.

Spoiler kode fungsi strlen standar di 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 


Tabel 1 - runtime dari tolok ukur strlen dalam hitungan detik (komunitas MS VS 2019, versi C ++ cl: 19.22.27905)

Blok besar, 1KBlok besar, 1K, * panggilan strlenBlok kecil, 10 elemenBlok kecil, 10 elemen, * panggilan strlen
Debug, x867.257.253.063.06
Rilis, x869.03.90,150,12
Debug, x646.06.03.43.4
Rilis, x648.52.30,150,11

* memaksa kompiler untuk memanggil fungsi pustaka strlen

Dengan demikian, kita dapat menyimpulkan bahwa substitusi perbandingan byte oleh kompiler MS tidak efisien bahkan pada string kecil, dan pada string besar, Debug berada di depan Release!

Kode Bench
 #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; } 


Tali
 len_result += strlen(str_for_test); 
kompilasi
Debug di: memanggil fungsi perpustakaan strlen;
Rilis dalam: perbandingan byte.

Jika Anda berkomentar dan menulisnya

 len_result += strlen_func(str_for_test); 

dimana

 auto strlen_func = strlen; 

kami akan memaksa kompiler untuk selalu memanggil fungsi perpustakaan.

Karena apa percepatan fungsi perpustakaan dicapai sebelum perbandingan byte sebesar 2,3 kali (untuk Rilis, x86, 1k)?

Karena perbandingan tidak dengan satu byte, tetapi langsung oleh 4. Semua keajaiban di sini:

 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 

Apakah mungkin melakukan lebih cepat menggunakan instruksi vektor prosesor modern? Ayo kita coba.

Menggunakan panduan Intel Intrinsics , kami menemukan _mm_cmpistri SSE4.2 intrinsik, yang dirancang hanya untuk bekerja dengan string. Dua vektor dengan panjang 128 bit dan penutup operasi diumpankan ke input. Sebagai mask, kami menggunakan: _SIDD_UBYTE_OPS = 0 - tipe data, _SIDD_CMP_EQUAL_EACH = 8 - operasi perataan byte, dan kami akan membandingkan dengan vektor nol. Nilai yang dikembalikan akan menjadi jumlah elemen tidak sama berpasangan pertama (yaitu, jika elemen cocok dari kiri ke kanan, penghitungan berhenti, saya akan senang jika seseorang mengkonfirmasi perilaku).

 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; 

Kode

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

Berfungsi untuk menyamakan alamat jalur yang dimuat, alamat tersebut diperlukan dalam kelipatan 16 agar sebagian besar instruksi SSE dapat berfungsi. Untuk instruksi pcmpistri yang digunakan oleh kami, penyelarasan benar-benar tidak diperlukan, pengecualian akses tidak akan dibuang.
Intrinsik
 __m128i data = _mm_load_si128((__m128i*)(str + length)); if ((res = _mm_cmpistri(z128, data, _SIDD_CMP_EQUAL_EACH)) != 16) 

Dikompilasi menjadi:
 pcmpistri xmm1,xmmword ptr [eax+edx],8 

Namun, menyelaraskan ke 16 juga berguna dalam kasus kami, ini memberikan sedikit peningkatan dalam kecepatan, dan jadi kami yakin bahwa siklus baca 16 byte tidak akan pergi ke halaman yang berpotensi tidak dialokasikan ( halaman memori 4K ).

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

"Mendapat" ukuran string jika ujungnya terdeteksi (karena saya tidak sepenuhnya yakin tentang algoritma operasi _mm_cmpistri).
dihapus setelah komentar picul , yang memberi peningkatan pada baris kecil.

Apakah kami membuat strlen tercepat? - Sayangnya, tidak, orang-orang dari https://www.strchr.com/sse2_optimised_strlen bahkan lebih cepat dan tanpa menggunakan SSE4.2.

Tabel 2 - runtime dari tolok ukur strlen dalam hitungan detik (Rilis)
Jumlah karakterPerbandingan MS byteMs strlenSSE 4.2SSE 2
10, x860,150,120,120,125
1 r, x869.03.91.651.42
10, x640,150,110,080,1
1 rb, x648.52.31.61.32

Kesimpulan:

Sepertinya saya bahwa MS selalu perlu memanggil library strlen, dan tidak melakukan perbandingan byte substitusi.

UPD
Menambahkan tes x64.
Menghapus loop terakhir di strlen_SSE4

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


All Articles