इसके लिए कुछ विवरण / पृष्ठभूमि के बारे में टिप्पणियों में बहुत (थोड़ा या पूरी तरह से) गलत अनुमान लगाया गया है।
आप glibc के अनुकूलित C फॉलबैक अनुकूलित कार्यान्वयन को देख रहे हैं । (आईएसएएस के लिए, जिनके पास हाथ से लिखा हुआ एसएसएम कार्यान्वयन नहीं है) । या उस कोड का एक पुराना संस्करण, जो अभी भी glibc स्रोत के पेड़ में है। https://code.woboq.org/userspace/glibc/string/strlen.c.html एक कोड-ब्राउज़र है जो वर्तमान ग्लिबिट गिट ट्री पर आधारित है। जाहिर है यह अभी भी MIPS सहित कुछ मुख्यधारा के glibc लक्ष्यों द्वारा उपयोग किया जाता है। (साभार @zwol)।
X86 और ARM जैसे लोकप्रिय ISAs पर, glibc हाथ से लिखे हुए asm का उपयोग करता है
इसलिए इस कोड के बारे में कुछ भी बदलने का प्रोत्साहन आपके विचार से कम है।
यह bithack कोड ( https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord ) वास्तव में आपके सर्वर / डेस्कटॉप / लैपटॉप / स्मार्टफोन पर नहीं चलता है। यह एक भोला- भाला -से-एक-समय लूप से बेहतर है, लेकिन आधुनिक सीपीयू (विशेष रूप से x86 जहां AVX2 SIMD एक जोड़े के निर्देशों के साथ 32 बाइट्स की जाँच करने की अनुमति देता है, 32 से 64 बाइट्स प्रति घड़ी की अनुमति देता है) मुख्य पाश में चक्र यदि 2 / घड़ी वेक्टर लोड और ALU थ्रूपुट के साथ आधुनिक CPU पर L1d कैश में डेटा गर्म है। यानी मध्यम आकार के तारों के लिए जहां स्टार्टअप ओवरहेड हावी नहीं होता है।)
glibc strlen
आपके CPU के लिए एक इष्टतम संस्करण को हल करने के लिए गतिशील लिंकिंग ट्रिक्स का उपयोग करता है , इसलिए यहां तक कि x86 के भीतर एक SSE2 संस्करण (16-बाइट वैक्टर, x86-64 के लिए आधारभूत) और एक AVX2 संस्करण (32-बाइट डॉक्टर्स) है।
x86 में वेक्टर और सामान्य-उद्देश्य रजिस्टरों के बीच कुशल डेटा ट्रांसफर होता है, जो कि SIMD का उपयोग करने के लिए निहित-लंबाई के तारों पर कार्यों को गति देने के लिए अच्छा है जहां लूप नियंत्रण डेटा पर निर्भर है। pcmpeqb
/ pmovmskb
एक बार में 16 अलग-अलग बाइट्स का परीक्षण करना संभव बनाता है।
glibc में AASchD का उपयोग करने जैसा एक AArch64 संस्करण है , और AArch64 CPUs के लिए एक संस्करण जहां वेक्टर-> GP रजिस्टर पाइप लाइन को रोकता है, इसलिए यह वास्तव में इस बिटकॉक का उपयोग करता है । लेकिन एक बार हिट होने के बाद बाइट-इन-रजिस्टर को खोजने के लिए काउंट-लीडिंग-जीरो का उपयोग करता है, और पेज-क्रॉसिंग के लिए जाँच के बाद AArch64 के कुशल अनलॉन्ग एक्सेस का लाभ उठाता है।
यह भी संबंधित: यह कोड 6.5x धीमा क्यों है जिसका अनुकूलन सक्षम है? strlen
एक बड़े बफ़र के साथ x86 asm में तेज़ या धीमा क्या है, इसके बारे में कुछ और विवरण हैं, एक सरल asm कार्यान्वयन जो कि इनलाइन को जानने के लिए gcc के लिए अच्छा हो सकता है। (कुछ gcc संस्करण अनचाहे रूप से इनलाइन rep scasb
जो बहुत धीमे हैं, या इस तरह का एक 4-बाइट-ए-टाइम बिटकॉक है। इसलिए GCC की इनलाइन-स्ट्रैलेन रेसिपी को अपडेट करने या अक्षम करने की आवश्यकता है।)
एसम के पास सी-शैली "अपरिभाषित व्यवहार" नहीं है ; मेमोरी में बाइट्स का उपयोग करना सुरक्षित है, लेकिन आप इसे पसंद करते हैं, और एक संरेखित लोड जिसमें कोई मान्य बाइट्स शामिल हैं, गलती नहीं कर सकता। स्मृति संरक्षण संरेखित-पृष्ठ दानेदारता के साथ होता है; संरेखित पहुँच संकरी की तुलना में पृष्ठ सीमा पार नहीं कर सकती है। क्या x86 और x64 पर एक ही पृष्ठ के भीतर एक बफर के अंत को पढ़ना सुरक्षित है? यही तर्क मशीन-कोड पर लागू होता है कि इस सी हैक को इस फ़ंक्शन के स्टैंड-अलोन नॉन-इनलाइन कार्यान्वयन के लिए बनाने के लिए कंपाइलर मिलते हैं।
जब कोई संकलक किसी अज्ञात नॉन-इनलाइन फ़ंक्शन को कॉल करने के लिए कोड का उत्सर्जन करता है, तो यह मानना होगा कि फ़ंक्शन किसी भी / सभी वैश्विक चर को संशोधित करता है और किसी भी मेमोरी में संभवतः इसके लिए एक संकेतक हो सकता है। स्थानीय लोगों को छोड़कर उनके पते से बच निकलने वाली हर चीज को कॉल के दौरान मेमोरी में सिंक करना पड़ता है। यह एएसएम में लिखे गए कार्यों पर लागू होता है, जाहिर है, लेकिन लाइब्रेरी के कार्यों के लिए भी। यदि आप लिंक-टाइम ऑप्टिमाइज़ेशन को सक्षम नहीं करते हैं, तो यह अलग अनुवाद इकाइयों (स्रोत फ़ाइलों) पर भी लागू होता है।
यह ग्लिबक के हिस्से के रूप में सुरक्षित क्यों है, लेकिन अन्यथा नहीं ।
सबसे महत्वपूर्ण कारक यह है कि यह strlen
किसी और चीज में प्रवेश नहीं कर सकता है। यह उसके लिए सुरक्षित नहीं है; इसमें यूबीबी ( char
डेटा को पढ़ने के माध्यम से unsigned long*
) सख्त-अलियासिंग शामिल है । char*
किसी और चीज को बदलने की अनुमति है, लेकिन रिवर्स सच नहीं है ।
यह फॉरवर्ड-ऑफ-टाइम संकलित पुस्तकालय (glibc) के लिए एक लाइब्रेरी फ़ंक्शन है। यह कॉलर्स में लिंक-टाइम-ऑप्टिमाइज़ेशन के साथ इनलेट नहीं होगा। इसका मतलब यह है कि यह बस स्टैंड-अलोन संस्करण के लिए सुरक्षित मशीन कोड को संकलित करना है strlen
। यह पोर्टेबल / सुरक्षित सी नहीं होना चाहिए
जीएनयू सी लाइब्रेरी को केवल जीसीसी के साथ संकलित करना है। जाहिरा तौर पर इसका समर्थन करने के लिए इसका समर्थन नहीं किया जाता है, भले ही वे GNU एक्सटेंशन का समर्थन करते हों। GCC एक समय-समय पर संकलक है जो C स्रोत फ़ाइल को मशीन कोड की ऑब्जेक्ट फ़ाइल में बदल देता है। एक दुभाषिया नहीं है, इसलिए जब तक यह संकलन समय पर नहीं आता है, तब तक स्मृति में बाइट्स केवल स्मृति में बाइट्स होते हैं। यानी सख्त-अलियासिंग यूबी खतरनाक नहीं है जब विभिन्न प्रकारों के साथ पहुंचें विभिन्न कार्यों में होती हैं जो एक दूसरे में प्रवेश नहीं करती हैं।
याद रखें कि आईएसओ सी मानक द्वाराstrlen
व्यवहार को परिभाषित किया गया है। यह फ़ंक्शन नाम विशेष रूप से कार्यान्वयन का हिस्सा है । जब तक आप उपयोग नहीं करते हैं -fno-builtin-strlen
, तब तक जीसीसी जैसे कंपाइलर एक अंतर्निहित फ़ंक्शन के रूप में नाम का इलाज करते हैं , इसलिए strlen("foo")
एक संकलन-समय स्थिर हो सकता है 3
। पुस्तकालय में परिभाषा का उपयोग केवल तब किया जाता है जब gcc वास्तव में स्वयं की रेसिपी या किसी चीज़ को सम्मिलित करने के बजाय उस पर कॉल करने का निर्णय लेता है।
जब यूबी संकलन समय पर संकलक को दिखाई नहीं देता है , तो आप समझदार मशीन कोड प्राप्त करते हैं। मशीन कोड को नो-यूबी मामले के लिए काम करना पड़ता है, और यहां तक कि अगर आप चाहते थे , तो यह पता लगाने का कोई तरीका नहीं है कि कॉलर किस प्रकार से डेटा को पॉइंट-इन मेमोरी में डालने के लिए उपयोग करता है।
Glibc को स्टैंड-अलोन स्थिर या गतिशील लाइब्रेरी के लिए संकलित किया गया है जो लिंक-टाइम ऑप्टिमाइज़ेशन के साथ इनलाइन नहीं कर सकता है। glibc की बिल्ड स्क्रिप्ट किसी प्रोग्राम में इनलाइन करते समय लिंक-टाइम ऑप्टिमाइज़ेशन के लिए मशीन कोड + gcc GIMPLE आंतरिक प्रतिनिधित्व वाली "वसा" स्थिर लाइब्रेरी नहीं बनाती है। (यानी मुख्य कार्यक्रम libc.a
में -flto
लिंक-टाइम ऑप्टिमाइज़ेशन में भाग नहीं लेंगे ।) इस तरह से बिल्डिंग का निर्माण उन लक्ष्यों पर.c
संभावित रूप से असुरक्षित होगा जो वास्तव में इसका उपयोग करते हैं ।
वास्तव में @zwol टिप्पणियों के रूप में, LTB का उपयोग स्वयं ग्लिबक के निर्माण के दौरान नहीं किया जा सकता है , क्योंकि "भंगुर" कोड इस तरह से होता है, जो अगर ग्लिबेक स्रोत फ़ाइलों के बीच इनलाइनिंग को तोड़ सकता है। (कुछ आंतरिक उपयोग हैं strlen
, उदाहरण के लिए printf
कार्यान्वयन के भाग के रूप में )
यह strlen
कुछ धारणाएँ बनाता है:
CHAR_BIT
8 का गुणक है । सभी GNU सिस्टम पर सही है। POSIX 2001 भी गारंटी देता है CHAR_BIT == 8
। (यह के साथ सिस्टम के लिए सुरक्षित दिखता है CHAR_BIT= 16
या 32
कुछ DSPs की तरह,; असंरेखित-प्रस्तावना पाश हमेशा 0 पुनरावृत्तियों अगर चलेंगे sizeof(long) = sizeof(char) = 1
, क्योंकि हर सूचक हमेशा गठबंधन है और p & sizeof(long)-1
हमेशा शून्य है।) लेकिन यदि आप एक गैर- ASCII वर्ण सेट था जहां वर्ण 9 हैं या 12 बिट्स चौड़ा, 0x8080...
गलत पैटर्न है।
- (शायद)
unsigned long
4 या 8 बाइट्स है। या शायद यह वास्तव में unsigned long
8 तक के किसी भी आकार के लिए काम करेगा , और यह assert()
उस के लिए जांच करने के लिए उपयोग करता है।
वे दो संभव यूबी नहीं हैं, वे कुछ सी कार्यान्वयन के लिए गैर-पोर्टेबिलिटी हैं। यह कोड उन प्लेटफार्मों पर सी कार्यान्वयन का हिस्सा है (या था) जहां यह काम करता है, इसलिए यह ठीक है।
अगली धारणा संभावित C UB है:
- एक संरेखित लोड जिसमें कोई वैध बाइट्स शामिल हैं , गलती नहीं कर सकता , और जब तक आप वास्तव में इच्छित वस्तु के बाहर बाइट्स को अनदेखा नहीं करते तब तक सुरक्षित है। (हर GNU सिस्टम पर और सभी सामान्य CPU पर asm में सही है, क्योंकि मेमोरी प्रोटेक्शन एलाइन पेज ग्रैन्युलैरिटी के साथ होता है। क्या U86 और x64 पर एक ही पेज के भीतर बफर के अंत को पढ़ना सुरक्षित है ? C में UB? संकलन समय पर दिखाई नहीं देता है। बिना इनलाइन किए, यह मामला यहां है। कंपाइलर यह साबित नहीं कर सकता है कि पहला पढ़ने
0
वाला यूबी है; यह उदाहरण के लिए एक सी char[]
सरणी युक्त हो सकता है {1,2,0,3}
)
वह अंतिम बिंदु वह है जो सी ऑब्जेक्ट के अंत में यहां पढ़ने के लिए सुरक्षित बनाता है। वर्तमान कंपाइलरों के साथ इनलाइन करते समय भी यह बहुत सुरक्षित है क्योंकि मुझे लगता है कि वे वर्तमान में ऐसा नहीं करते हैं कि निष्पादन का एक रास्ता असंभव है। लेकिन वैसे भी, सख्त अलियासिंग पहले से ही एक शोस्टॉपर है अगर आपने कभी इस इनलाइन को होने दिया।
फिर आपको लिनक्स कर्नेल के पुराने असुरक्षित memcpy
CPP मैक्रो जैसी समस्याएं होंगी, जो पॉइंटर-कास्टिंग से लेकर unsigned long
( gcc, सख्त- aliasing, और डरावनी कहानियों ) का उपयोग करती हैं।
यह strlen
उस युग में आता है जब आप सामान के साथ भाग सकते थे ; यह जीसीसी 3 से पहले "केवल जब इनलाइनिंग नहीं" कैविटी के बिना बहुत अधिक सुरक्षित हुआ करता था।
UB जो केवल तभी दिखाई देता है जब कॉल / रिट सीमाएं हमें दिखाई देती हैं। (उदाहरण के लिए एक डाली char buf[]
पर एक सरणी के बजाय इस पर कॉल unsigned long[]
करना const char*
)। एक बार मशीन कोड पत्थर में सेट हो जाने के बाद, यह सिर्फ बाइट्स के साथ मेमोरी में काम कर रहा है। एक गैर-इनलाइन फ़ंक्शन कॉल को यह मान लेना है कि कैली किसी भी / सभी मेमोरी को पढ़ता है।
सख्ती से-उर्फ यूबी के बिना, यह सुरक्षित रूप से लिखना
जीसीसी प्रकार विशेषताmay_alias
एक प्रकार के रूप में एक ही उपनाम-कुछ भी उपचार देता है char*
। (@KonradBorowsk द्वारा सुझाया गया)। जीसीसी हेडर वर्तमान में इसे x86 SIMD वेक्टर प्रकारों के लिए उपयोग करते हैं, जैसे __m128i
कि आप हमेशा सुरक्षित रूप से कर सकते हैं _mm_loadu_si128( (__m128i*)foo )
। (देखें कि हार्डवेयर वेक्टर पॉइंटर और संबंधित प्रकार के अपरिभाषित व्यवहार के बीच `reinterpret_cast`ing है , यह क्या करता है और इसका मतलब नहीं है के बारे में अधिक जानकारी के लिए।)
strlen(const char *char_ptr)
{
typedef unsigned long __attribute__((may_alias)) aliasing_ulong;
aliasing_ulong *longword_ptr = (aliasing_ulong *)char_ptr;
for (;;) {
unsigned long ulong = *longword_ptr++; // can safely alias anything
...
}
}
आप aligned(1)
एक प्रकार के साथ व्यक्त करने के लिए भी उपयोग कर सकते हैं alignof(T) = 1
।
typedef unsigned long __attribute__((may_alias, aligned(1))) unaligned_aliasing_ulong;
आईएसओ में एक अलियासिंग लोड को व्यक्त करने का एक पोर्टेबल तरीका हैmemcpy
, जो आधुनिक संकलक जानते हैं कि एक एकल लोड निर्देश के रूप में इनलाइन कैसे करें। जैसे
unsigned long longword;
memcpy(&longword, char_ptr, sizeof(longword));
char_ptr += sizeof(longword);
यह अन memcpy
- असाइन किए गए लोड के लिए भी काम करता है क्योंकि जैसे-अगर char
-ए-ए-टाइम एक्सेस के रूप में काम करता है । लेकिन व्यवहार में आधुनिक संकलक memcpy
बहुत अच्छी तरह से समझते हैं ।
यहां खतरा यह है कि अगर जीसीसी को यह पता नहीं है कि char_ptr
यह शब्द-संरेखित है, तो यह कुछ प्लेटफार्मों पर इनलाइन नहीं करेगा जो कि असम्बद्ध भार का समर्थन नहीं कर सकते हैं। MIPS64r6 से पहले MIPS, या पुराने ARM जैसे। यदि आपको memcpy
एक शब्द लोड करने के लिए एक वास्तविक फ़ंक्शन कॉल मिला है (और इसे अन्य मेमोरी में छोड़ दें), तो यह एक आपदा होगी। जब कोड पॉइंटर संरेखित करता है, तो जीसीसी कभी-कभी देख सकता है। या चार-एक-समय के लूप के बाद जो एक लंबी सीमा तक पहुंचता है जो आप उपयोग कर सकते हैं
p = __builtin_assume_aligned(p, sizeof(unsigned long));
यह रीड-पास्ट-द-ऑब्जेक्ट ऑब्जेक्ट यूबी से बचता नहीं है, लेकिन वर्तमान जीसीसी के साथ जो व्यवहार में खतरनाक नहीं है।
हाथ से अनुकूलित सी स्रोत क्यों आवश्यक है: वर्तमान संकलक पर्याप्त अच्छे नहीं हैं
जब आप व्यापक रूप से उपयोग किए जाने वाले मानक लाइब्रेरी फ़ंक्शन के लिए प्रदर्शन के प्रत्येक अंतिम ड्रॉप चाहते हैं तो हाथ से अनुकूलित एएसएम और भी बेहतर हो सकता है। विशेष रूप से कुछ के लिए memcpy
, लेकिन यह भी strlen
। इस स्थिति में SSE2 का लाभ उठाने के लिए x86 आंतरिक के साथ C का उपयोग करना बहुत आसान नहीं होगा।
लेकिन यहाँ हम बिना किसी आईएसए-विशिष्ट सुविधाओं के बस एक भोले बनाम बिटक सी संस्करण के बारे में बात कर रहे हैं।
(मुझे लगता है कि हम इसे एक दिए गए के रूप में ले सकते हैं जो strlen
व्यापक रूप से पर्याप्त रूप से उपयोग किया जाता है जो इसे जितना संभव हो उतना तेजी से चलाने के लिए महत्वपूर्ण है। इसलिए यह सवाल बन जाता है कि क्या हम सरल स्रोत से कुशल मशीन कोड प्राप्त कर सकते हैं। नहीं, हम नहीं कर सकते हैं।)
वर्तमान जीसीसी और क्लैंग ऑटो-वेक्टरिंग लूप्स में सक्षम नहीं हैं, जहां पुनरावृत्ति गिनती पहले पुनरावृत्ति से आगे नहीं जानी जाती है । (उदाहरण के लिए यह जांचना संभव है कि क्या लूप पहले पुनरावृत्ति को चलाने से पहले कम से कम 16 पुनरावृत्तियों को चलाएगा ।) उदाहरण के लिए ऑटोवैक्टराइजिंग मेम्पी संभव है (स्पष्ट-लंबाई बफर) लेकिन वर्तमान को देखते हुए स्ट्रैची या स्ट्रलेन (अंतर्निहित-लंबाई स्ट्रिंग) नहीं। compilers।
जिसमें खोज लूप, या डेटा-निर्भर के if()break
साथ-साथ काउंटर के साथ कोई अन्य लूप शामिल है ।
ICC (x86 के लिए इंटेल का संकलक) कुछ खोज छोरों को ऑटो-वेक्टर कर सकता है, लेकिन फिर भी केवल strlen
ओपनबीडी के लिबास जैसे साधारण / भोले सी के लिए भोले-से-एक-बार का उपयोग करता है। ( गॉडबोल्ट )। ( @ पेसके के जवाब से )।
strlen
वर्तमान संकलक के साथ प्रदर्शन के लिए एक हाथ से अनुकूलित परिवाद आवश्यक है । एक बार में 1 बाइट जाना (हो सकता है कि व्यापक सुपरसर्कर सीपीयू पर प्रति चक्र 2 बाइट्स को अनियंत्रित करना) दयनीय हो जब मुख्य मेमोरी लगभग 8 बाइट प्रति चक्र के साथ रख सकती है, और एल 1 डी कैश 16 से 64 प्रति चक्र वितरित कर सकता है। (2x 32-बाइट लोड प्रति चक्र आधुनिक मुख्यधारा x86 सीपीयू पर हैसवेल और राइज़ेन के बाद से। मतगणना AVX512 जो केवल 512-बिट वैक्टर का उपयोग करने के लिए घड़ी की गति को कम कर सकती है; यही कारण है कि glibc शायद एक AVX512 संस्करण को जोड़ने की जल्दी में नहीं है; । हालांकि, 256-बिट वैक्टर के साथ, AVX512VL + BW मास्क की तुलना एक मास्क में की जाती है और ktest
या इसके यूओपी / पुनरावृत्ति को कम करके अधिक हाइपरथ्रेडिंग फ्रेंडली kortest
बना सकता है strlen
।)
मैं यहाँ गैर x86 को शामिल कर रहा हूँ, यह "16 बाइट्स" है। उदाहरण के लिए सबसे AArch64 CPUs कम से कम ऐसा कर सकते हैं, मुझे लगता है, और कुछ निश्चित रूप से अधिक। और कुछ के पास strlen
उस लोड बैंडविड्थ के साथ रखने के लिए पर्याप्त निष्पादन थ्रूपुट है ।
बेशक प्रोग्राम जो बड़े स्ट्रिंग्स के साथ काम करते हैं, उन्हें आमतौर पर लंबाई का ट्रैक रखना चाहिए ताकि इन-लेंथ सी स्ट्रिंग्स की लंबाई का पता लगाने में बहुत कम समय लगे। लेकिन लघु से मध्यम लंबाई के प्रदर्शन अभी भी हाथ से लिखे हुए कार्यान्वयन से लाभान्वित होते हैं, और मुझे यकीन है कि कुछ कार्यक्रम मध्यम-लंबाई के स्ट्रिंग्स पर स्ट्रलेन का उपयोग करके समाप्त होते हैं।