كيف لا تجعل أسرع strlen والعثور على خطأ في Visual Studio 2019 الجماعة

لقد طُلب مني مقال حول استخدام تعليمة popcount "الغريبة" في المعالجات الحديثة . لا يتعلق الأمر بحساب عدد الوحدات ، بل يتعلق بالكشف عن علامة نهاية خط C (خط منتهي الصلاحية ).
سلسلة منتهية بقيمة خالية هي وسيلة لتمثيل السلاسل في لغات البرمجة ، حيث يتم استخدام مجموعة من الأحرف بدلاً من إدخال نوع سلسلة خاص ، وتكون نهاية السلسلة هي أول حرف فارغ خاص تمت مصادفته (NUL من رمز ASCII ، بقيمة 0).

لتحديد طول هذا المصطلح ، يتم تطبيق وظيفة قياسية.

size_t __cdecl strlen(char const* str) 

يمكن وصف خوارزمية التشغيل في C على النحو التالي:

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

دعونا نرى ما يحوله مترجم مجتمع MS Visual Studio 2019 (الإصدار ، x86) إلى:

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

بمعنى ، يتم تحميل بايت واحد من الذاكرة ومقارنة مع صفر. يتم استبدال نفس الرمز لمكالمات strlen إذا قمت بإنشاء المشروع في Release ، الخوارزمية صحيحة ، لكن السرعة ، كما يبدو لي ، ليست كافية. ماذا يحدث إذا قمت ترجمة التعليمات البرمجية مع استدعاء strlen القياسية في Debug؟ - سيتم استدعاء وظيفة مكتبة strlen ، كما هو متوقع ، ولكن مكتوبة من قبل شخص يدويا على المجمع.

spoiler رمز الدالة strlen القياسية في 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 


الجدول 1 - وقت تشغيل معيار strlen بالثواني (مجتمع MS VS 2019 ، الإصدار C ++ cl: 19.22.27905)

كتلة كبيرة ، 1Kكتلة كبيرة ، 1K ، * دعوة strlenكتلة صغيرة ، 10 عناصركتلة صغيرة ، 10 عناصر ، * دعوة strlen
تصحيح ، إلى x867.257.253.063.06
الإصدار ، x86تسعة3.90.150.12
تصحيح ، إلى x646.06.03.43.4
الإصدار ، إلى x648.52.30.150.11

* فرض المترجم لاستدعاء وظيفة مكتبة strlen

وبالتالي ، يمكننا أن نستنتج أن استبدال مقارنة البايت بواسطة برنامج التحويل البرمجي MS غير فعال حتى في السلاسل الصغيرة ، وفي السلاسل الكبيرة ، Debug قبل الإصدار!

كود مقاعد البدلاء
 #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; } 


صف
 len_result += strlen(str_for_test); 
جمعت
تصحيح في: استدعاء دالة مكتبة strlen؛
الإصدار في: مقارنة البايت.

إذا قمت بالتعليق والكتابة

 len_result += strlen_func(str_for_test); 

حيث

 auto strlen_func = strlen; 

سنقوم بإجبار المترجم على استدعاء وظيفة المكتبة دائمًا.

نظرًا لما تحقق من تسريع وظيفة المكتبة قبل مقارنة البايت بمقدار 2.3 مرة (بالنسبة للإصدار x86 و 1 k)؟

بسبب المقارنة ليس بواسطة بايت واحد ، ولكن على الفور عن طريق 4. كل السحر هنا:

 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 

هل من الممكن أن تفعل بشكل أسرع باستخدام تعليمات ناقلات المعالجات الحديثة؟ لنجربها.

باستخدام دليل Intel Intrinsics ، نعثر على _mm_cmpistri SSE4.2 الجوهري ، المصمم للعمل مع السلاسل فقط. يتم إدخال متجهين بطول 128 بت وقناع عمليات إلى الإدخال. كقناع نستخدم: _SIDD_UBYTE_OPS = 0 - نوع البيانات ، _SIDD_CMP_EQUAL_EACH = 8 - تشغيل محاذاة البايت ، وسنقوم بمقارنتها مع متجه صفري. ستكون القيمة التي يتم إرجاعها هي عدد العناصر غير المتكافئة الزوجية الأولى (أي إذا تطابق العنصر من اليسار إلى اليمين ، يتوقف العد ، وسأكون سعيدًا إذا أكد شخص ما السلوك).

 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; 

قانون

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

يعمل على معادلة عنوان الخط الذي تم تحميله ، هناك حاجة إلى العنوان في مضاعفات 16 حتى تعمل معظم تعليمات SSE. بالنسبة لتعليمات pcmpistri التي نستخدمها ، فإن المحاذاة ليست ضرورية تمامًا ، فلن يتم طرح استثناء وصول.
Intrinsiki
 __m128i data = _mm_load_si128((__m128i*)(str + length)); if ((res = _mm_cmpistri(z128, data, _SIDD_CMP_EQUAL_EACH)) != 16) 

جمعت في:
 pcmpistri xmm1,xmmword ptr [eax+edx],8 

ومع ذلك ، تعد المحاذاة إلى 16 مفيدة أيضًا في حالتنا ، فهي توفر زيادة بسيطة في السرعة ، وبالتالي نحن على يقين من أن دورة القراءة التي تبلغ 16 بايت لن تنتقل إلى صفحة غير محتملة ( صفحة ذاكرة 4K ).

دورة:
 while (str[length]) length++; 

"يحصل" على حجم السلسلة إذا تم الكشف عن نهايتها (حيث أنني لست متأكدًا تمامًا من خوارزمية العملية _mm_cmpistri).
حذف بعد تعليق picul ، مما أعطى زيادة على خطوط صغيرة.

هل حققنا أسرع عملية؟ - لسوء الحظ ، لا ، لقد فعل الرجال من https://www.strchr.com/sse2_optimised_strlen بشكل أسرع وبدون استخدام SSE4.2.

الجدول 2 - وقت تشغيل معيار strlen بالثواني (الإصدار)
عدد الشخصياتمقارنة MS بايتالسيدة سترلينSSE 4.2SSE 2
10 ، x860.150.120.120.125
1 ك و x86تسعة3.91.651.42
10 ، إلى 640.150.110.080.1
1 ك ، إلى 648.52.31.61.32

الاستنتاجات:

يبدو لي أن مرض التصلب العصبي المتعدد يحتاج دائما إلى استدعاء مكتبة strlen ، وليس القيام مقارنات بايت الاستبدال.

UPD.
أضيف إلى x64 اختبار.
تمت إزالة الحلقة الأخيرة في strlen_SSE4

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


All Articles