ग्लिब्स की स्ट्रैलेन को जल्दी से चलाने के लिए इतना जटिल होने की आवश्यकता क्यों है?


286

मैं यहांstrlen कोड के माध्यम से देख रहा था और मैं सोच रहा था कि क्या कोड में उपयोग किए गए अनुकूलन वास्तव में आवश्यक हैं? उदाहरण के लिए, निम्न कार्य समान रूप से अच्छा या बेहतर क्यों नहीं होगा?

unsigned long strlen(char s[]) {
    unsigned long i;
    for (i = 0; s[i] != '\0'; i++)
        continue;
    return i;
}

कंपाइलर का अनुकूलन करने के लिए सरल कोड बेहतर और / या आसान नहीं है?

strlenलिंक के पीछे पृष्ठ पर कोड इस तरह दिखता है:

/* Copyright (C) 1991, 1993, 1997, 2000, 2003 Free Software Foundation, Inc.
   This file is part of the GNU C Library.
   Written by Torbjorn Granlund (tege@sics.se),
   with help from Dan Sahlin (dan@sics.se);
   commentary by Jim Blandy (jimb@ai.mit.edu).

   The GNU C Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   The GNU C Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with the GNU C Library; if not, write to the Free
   Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
   02111-1307 USA.  */

#include <string.h>
#include <stdlib.h>

#undef strlen

/* Return the length of the null-terminated string STR.  Scan for
   the null terminator quickly by testing four bytes at a time.  */
size_t
strlen (str)
     const char *str;
{
  const char *char_ptr;
  const unsigned long int *longword_ptr;
  unsigned long int longword, magic_bits, himagic, lomagic;

  /* Handle the first few characters by reading one character at a time.
     Do this until CHAR_PTR is aligned on a longword boundary.  */
  for (char_ptr = str; ((unsigned long int) char_ptr
            & (sizeof (longword) - 1)) != 0;
       ++char_ptr)
    if (*char_ptr == '\0')
      return char_ptr - str;

  /* All these elucidatory comments refer to 4-byte longwords,
     but the theory applies equally well to 8-byte longwords.  */

  longword_ptr = (unsigned long int *) char_ptr;

  /* Bits 31, 24, 16, and 8 of this number are zero.  Call these bits
     the "holes."  Note that there is a hole just to the left of
     each byte, with an extra at the end:

     bits:  01111110 11111110 11111110 11111111
     bytes: AAAAAAAA BBBBBBBB CCCCCCCC DDDDDDDD

     The 1-bits make sure that carries propagate to the next 0-bit.
     The 0-bits provide holes for carries to fall into.  */
  magic_bits = 0x7efefeffL;
  himagic = 0x80808080L;
  lomagic = 0x01010101L;
  if (sizeof (longword) > 4)
    {
      /* 64-bit version of the magic.  */
      /* Do the shift in two steps to avoid a warning if long has 32 bits.  */
      magic_bits = ((0x7efefefeL << 16) << 16) | 0xfefefeffL;
      himagic = ((himagic << 16) << 16) | himagic;
      lomagic = ((lomagic << 16) << 16) | lomagic;
    }
  if (sizeof (longword) > 8)
    abort ();

  /* Instead of the traditional loop which tests each character,
     we will test a longword at a time.  The tricky part is testing
     if *any of the four* bytes in the longword in question are zero.  */
  for (;;)
    {
      /* We tentatively exit the loop if adding MAGIC_BITS to
     LONGWORD fails to change any of the hole bits of LONGWORD.

     1) Is this safe?  Will it catch all the zero bytes?
     Suppose there is a byte with all zeros.  Any carry bits
     propagating from its left will fall into the hole at its
     least significant bit and stop.  Since there will be no
     carry from its most significant bit, the LSB of the
     byte to the left will be unchanged, and the zero will be
     detected.

     2) Is this worthwhile?  Will it ignore everything except
     zero bytes?  Suppose every byte of LONGWORD has a bit set
     somewhere.  There will be a carry into bit 8.  If bit 8
     is set, this will carry into bit 16.  If bit 8 is clear,
     one of bits 9-15 must be set, so there will be a carry
     into bit 16.  Similarly, there will be a carry into bit
     24.  If one of bits 24-30 is set, there will be a carry
     into bit 31, so all of the hole bits will be changed.

     The one misfire occurs when bits 24-30 are clear and bit
     31 is set; in this case, the hole at bit 31 is not
     changed.  If we had access to the processor carry flag,
     we could close this loophole by putting the fourth hole
     at bit 32!

     So it ignores everything except 128's, when they're aligned
     properly.  */

      longword = *longword_ptr++;

      if (
#if 0
      /* Add MAGIC_BITS to LONGWORD.  */
      (((longword + magic_bits)

        /* Set those bits that were unchanged by the addition.  */
        ^ ~longword)

       /* Look at only the hole bits.  If any of the hole bits
          are unchanged, most likely one of the bytes was a
          zero.  */
       & ~magic_bits)
#else
      ((longword - lomagic) & himagic)
#endif
      != 0)
    {
      /* Which of the bytes was the zero?  If none of them were, it was
         a misfire; continue the search.  */

      const char *cp = (const char *) (longword_ptr - 1);

      if (cp[0] == 0)
        return cp - str;
      if (cp[1] == 0)
        return cp - str + 1;
      if (cp[2] == 0)
        return cp - str + 2;
      if (cp[3] == 0)
        return cp - str + 3;
      if (sizeof (longword) > 4)
        {
          if (cp[4] == 0)
        return cp - str + 4;
          if (cp[5] == 0)
        return cp - str + 5;
          if (cp[6] == 0)
        return cp - str + 6;
          if (cp[7] == 0)
        return cp - str + 7;
        }
    }
    }
}
libc_hidden_builtin_def (strlen)

यह संस्करण जल्दी क्यों चलता है?

क्या यह अनावश्यक काम नहीं कर रहा है?


2
टिप्पणियाँ विस्तारित चर्चा के लिए नहीं हैं; इस वार्तालाप को बातचीत में स्थानांतरित कर दिया गया है ।
सैमुअल एलवाईई

18
भविष्य के संदर्भ के लिए, GNU libc के लिए आधिकारिक स्रोत भंडार < sourceware.org/git/?p=glibc.git > पर है। < sourceware.org/git/?p=glibc.it;a=blob;f=string/… > वास्तव में उपरोक्त के समान कोड दिखाता है; हालाँकि, sysdepsनिर्देशिका से हाथ से लिखी जाने वाली असेंबली भाषा के कार्यान्वयन का उपयोग ग्लिबक के अधिकांश समर्थित आर्किटेक्चर (आमतौर पर सबसे अधिक इस्तेमाल किया जाने वाला आर्किटेक्चर, जिसमें MIPS नहीं होता है) पर किया जाएगा।
zwol

9
इसे मुख्य रूप से राय-आधारित के रूप में बंद करने की वोटिंग; "क्या xxx में वास्तव में xxx की आवश्यकता है?" लोगों की राय के लिए व्यक्तिपरक है।
एसएस ऐनी

2
@ JL2210: अच्छी बात है, शीर्षक की एक शीर्षक में सवाल की भावना पर कब्जा करने के लिए शीर्षक निर्धारित किया गया है कि ऐसा लगता नहीं है कि यह सोच रहा है कि प्रदर्शन की आवश्यकता है, बस हमें प्रदर्शन पाने के लिए इन अनुकूलन की आवश्यकता क्यों है ।
पीटर कॉर्डेस

9
@ JL2210 FWIW, मूल शीर्षक था "C [sic!] में strlen इतना जटिल क्यों है, और यह" बहुत व्यापक "के रूप में बंद हो गया, फिर से खुल गया, फिर" मुख्य रूप से राय-आधारित "के रूप में बंद हो गया। मैंने इसे ठीक करने का प्रयास किया ("आपने मेरा प्रश्न तोड़ दिया!" और "आप लोग अपनी संपादन शक्तियों का दुरुपयोग कर रहे हैं!" इस बीच), लेकिन IMVHO ने समस्या के मूल आधार पर झूठ बोला (और अभी भी झूठ है)। जो समस्याग्रस्त था ("यह कोड मेरे लिए समझने के लिए बहुत जटिल है" प्रश्नोत्तर के लिए अच्छी तरह से अनुकूल नहीं है - IMO यह ट्यूशन के लिए एक अनुरोध है, उत्तर के लिए नहीं)। मैं इसे 60 फुट के ध्रुव के साथ फिर से नहीं छू रहा हूं :)

जवाबों:


233

आपको जरूरत नहीं है और आपको कभी भी ऐसा कोड नहीं लिखना चाहिए - खासकर यदि आप सी कंपाइलर / मानक लाइब्रेरी विक्रेता नहीं हैं। यह strlenकुछ बहुत ही संदिग्ध गति हैक और मान्यताओं के साथ लागू करने के लिए उपयोग किया जाता है (जो कि टिप्पणियों के साथ परीक्षण नहीं किया गया है या टिप्पणियों में उल्लिखित है:

  • unsigned long 4 या 8 बाइट्स है
  • बाइट्स 8 बिट हैं
  • एक पॉइंटर को कास्ट किया जा सकता है unsigned long longऔर नहींuintptr_t
  • एक पॉइंटर को केवल यह जाँच कर संरेखित कर सकता है कि 2 या 3 सबसे कम ऑर्डर बिट्स शून्य हैं
  • एक unsigned longएस के रूप में एक स्ट्रिंग का उपयोग कर सकते हैं
  • कोई किसी भी बुरे प्रभाव के बिना सरणी के अंत को पढ़ सकता है।

क्या अधिक है, एक अच्छा संकलक भी कोड के रूप में लिखा की जगह ले सकता है

size_t stupid_strlen(const char s[]) {
    size_t i;
    for (i=0; s[i] != '\0'; i++)
        ;
    return i;
}

(ध्यान दें कि इसे size_tकंपाइलर बिलिन के एक इनलाइन संस्करण के साथ एक प्रकार के अनुरूप होना चाहिए ) strlenया कोड को वेक्टर करना; लेकिन एक कंपाइलर जटिल संस्करण का अनुकूलन करने में सक्षम होने की संभावना नहीं होगी।


strlenसमारोह द्वारा वर्णित है C11 7.24.6.3 के रूप में:

विवरण

  1. strlenसमारोह स्ट्रिंग द्वारा की ओर इशारा की लंबाई गणना करता है।

रिटर्न

  1. यह strlenफ़ंक्शन उन वर्णों की संख्या लौटाता है जो समाप्त करने वाले अशक्त चरित्र से पहले हैं।

अब, यदि स्ट्रिंग द्वारा इंगित की गई sस्ट्रिंग और वर्णों को समाप्त करने के लिए वर्णों की एक सरणी में बस लंबे समय तक था, और NUL को समाप्त करने के लिए, व्यवहार को अनिर्धारित किया जाएगा यदि हम स्ट्रिंग पिछले शून्य टर्मिनेटर तक पहुंचते हैं, उदाहरण के लिए

char *str = "hello world";  // or
char array[] = "hello world";

तो वास्तव में पूरी तरह से पोर्टेबल / मानकों में एकमात्र तरीका सी को सही ढंग से लागू करने का तरीका है जो आपके प्रश्न में लिखा गया है , तुच्छ परिवर्तनों को छोड़कर - आप लूप आदि को अनियंत्रित करके तेज होने का दिखावा कर सकते हैं, लेकिन इसे अभी भी करने की आवश्यकता है एक बार में एक बाइट

(जैसा कि टिप्पणीकारों ने बताया है, जब सख्त पोर्टेबिलिटी बहुत अधिक बोझ है, उचित या ज्ञात-सुरक्षित मान्यताओं का लाभ उठाना हमेशा एक बुरी बात नहीं है। विशेष रूप से कोड में जो एक विशिष्ट सी कार्यान्वयन का हिस्सा है । लेकिन आपको समझना होगा) यह जानने से पहले कि आप उन्हें कैसे / कब रोक सकते हैं।


लिंक्ड strlenइंप्लीमेंट पहले बाइट्स को व्यक्तिगत रूप से जांचता है जब तक कि पॉइंटर नेचुरल 4 या 8 बाइट संरेखण सीमा की ओर इशारा नहीं करता है unsigned long। सी मानक का कहना है कि एक पॉइंटर को एक्सेस करना जो ठीक से संरेखित नहीं है , अपरिभाषित व्यवहार है , इसलिए यह पूरी तरह से अगले गंदे चाल के लिए और भी अधिक गंदा होना है। (कुछ सीपीयू 86 के अलावा अन्य वास्तुकला पर व्यवहार में, अनमेल शब्द या doubleword लोड गलती होगी। सी है नहीं एक पोर्टेबल विधानसभा भाषा है, लेकिन इस कोड इसे उस तरह से उपयोग कर रहा है)। यह भी है कि क्या यह संभव है कि किसी ऑब्जेक्ट के अंत को लागू करने के जोखिम के बिना लागू किया जा सकता है जहां कार्यान्वयन ब्लॉक में मेमोरी प्रोटेक्शन काम करता है (जैसे 4kiB वर्चुअल मेमोरी पेज)।

अब गंदा हिस्सा आता है: कोड वादा तोड़ता है और एक समय (ए long int) पर 4 या 8 8-बिट बाइट्स पढ़ता है , और अगर जल्दी से 4 या 8 के भीतर कोई भी शून्य बाइट्स थे, तो यह पता लगाने के लिए अहस्ताक्षरित जोड़ के साथ एक बिट चाल का उपयोग करता है। बाइट्स - यह एक विशेष रूप से तैयार की गई संख्या का उपयोग करता है जो कि बिट को बिट मास्क द्वारा पकड़े जाने वाले बिट्स को बदलने के लिए ले जाएगा। संक्षेप में यह तब पता लगाएगा कि यदि मुखौटा में 4 या 8 बाइट्स में से कोई भी शून्य हो, तो इन बाइट्स में से प्रत्येक के माध्यम से लूपिंग की तुलना में तेजी से होता है। अंत में यह पता लगाने के लिए अंत में एक लूप है कि कौन सा बाइट पहला शून्य था, यदि कोई हो, और परिणाम वापस करने के लिए।

सबसे बड़ी समस्या यह है कि sizeof (unsigned long) - 1कई बार sizeof (unsigned long)मामलों में यह स्ट्रिंग के अंत में पढ़ा जाएगा - केवल तभी जब नल बाइट अंतिम एक्सेस बाइट में होता है (यानी छोटे-एंडियन में सबसे महत्वपूर्ण, और बड़े-एंडियन में सबसे कम महत्वपूर्ण) , यह सीमा से बाहर सरणी का उपयोग नहीं करता है !


कोड, भले ही strlenC मानक लाइब्रेरी में लागू करने के लिए उपयोग किया जाता है, खराब कोड है। इसमें कई कार्यान्वयन-परिभाषित और अपरिभाषित पहलू हैं और सिस्टम-प्रदान के बजाय इसका कहीं भी उपयोग नहीं किया जाना चाहिए strlen- मैंने फ़ंक्शन का नाम बदलकर the_strlenयहां जोड़ा और निम्नलिखित को जोड़ा main:

int main(void) {
    char buf[12];
    printf("%zu\n", the_strlen(fgets(buf, 12, stdin)));
}

बफर सावधानीपूर्वक आकार है ताकि यह बिल्कुल hello worldस्ट्रिंग और टर्मिनेटर को पकड़ सके। हालाँकि मेरे 64-बिट प्रोसेसर पर unsigned long8 बाइट्स हैं, इसलिए बाद वाले हिस्से की पहुँच इस बफर से अधिक होगी।

मैं अब के साथ संकलन हैं -fsanitize=undefinedऔर -fsanitize=addressऔर जिसके परिणामस्वरूप कार्यक्रम चलाने के लिए, मैं मिलता है:

% ./a.out
hello world
=================================================================
==8355==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffffe63a3f8 at pc 0x55fbec46ab6c bp 0x7ffffe63a350 sp 0x7ffffe63a340
READ of size 8 at 0x7ffffe63a3f8 thread T0
    #0 0x55fbec46ab6b in the_strlen (.../a.out+0x1b6b)
    #1 0x55fbec46b139 in main (.../a.out+0x2139)
    #2 0x7f4f0848fb96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
    #3 0x55fbec46a949 in _start (.../a.out+0x1949)

Address 0x7ffffe63a3f8 is located in stack of thread T0 at offset 40 in frame
    #0 0x55fbec46b07c in main (.../a.out+0x207c)

  This frame has 1 object(s):
    [32, 44) 'buf' <== Memory access at offset 40 partially overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow (.../a.out+0x1b6b) in the_strlen
Shadow bytes around the buggy address:
  0x10007fcbf420: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007fcbf430: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007fcbf440: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007fcbf450: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007fcbf460: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x10007fcbf470: 00 00 00 00 00 00 00 00 00 00 f1 f1 f1 f1 00[04]
  0x10007fcbf480: f2 f2 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007fcbf490: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007fcbf4a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007fcbf4b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007fcbf4c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==8355==ABORTING

यानी बुरा काम हुआ।


120
पुन: "बहुत ही संदिग्ध गति हैक और मान्यताओं" - अर्थात, पोर्टेबल कोड में बहुत ही संदिग्ध । मानक पुस्तकालय एक विशेष संकलक / हार्डवेयर संयोजन के लिए लिखा जाता है, जिसमें चीजों की वास्तविक व्यवहार के ज्ञान के साथ भाषा की परिभाषा अपरिभाषित होती है। हां, अधिकांश लोगों को उस तरह का कोड लिखना नहीं चाहिए, लेकिन मानक पुस्तकालय को लागू करने के संदर्भ में गैर-पोर्टेबल स्वाभाविक रूप से खराब नहीं है।
पीट बेकर

4
सहमत हूं, इस तरह की चीजें कभी खुद न लिखें। या लगभग कभी नहीं। समयपूर्व अनुकूलन सभी बुराई का स्रोत है। (इस मामले में हालांकि यह वास्तव में प्रेरित हो सकता है)। यदि आप एक ही बहुत लंबे स्ट्रिंग पर बहुत सारे स्ट्रलेन () कॉल करते हैं, तो आपका आवेदन शायद अलग तरह से लिखा जा सकता है। आप उदाहरण के रूप में माइग्रेट करते हैं, जब स्ट्रिंग बनाई जाती है तो पहले से ही एक चर में स्ट्रिंग को बचाते हैं, और स्ट्रैलेन () को कॉल करने की आवश्यकता नहीं होती है।
ग्लीक्विस्ट

65
@ghellquist: अक्सर उपयोग की जाने वाली लाइब्रेरी कॉल का अनुकूलन करना शायद ही "समयपूर्व अनुकूलन" है।
jamesqf

7
@ शांति हापाला: बिल्कुल क्यों आपको लगता है कि स्ट्रलेन ओ (1) होना चाहिए? और हमारे पास यहां कई कार्यान्वयन हैं, जिनमें से सभी ओ (एन) हैं, लेकिन विभिन्न निरंतर गुणकों के साथ। आप यह नहीं सोच सकते कि यह मायने रखता है, लेकिन हम में से कुछ के लिए एक ओ (एन) एल्गोरिदम का कार्यान्वयन जो माइक्रोसेकंड में अपना काम करता है वह सेकंड, या यहां तक ​​कि मिलीसेकंड लेने वाले एक से बेहतर है, क्योंकि इसे कई अरब बार कहा जा सकता है। नौकरी का कोर्स।
jamesqf

8
@PeteBecker: न केवल मानक पुस्तकालयों (हालांकि इस उदाहरण में इतना है) के संदर्भ में, नॉनपोर्टेबल कोड लिखना मानक हो सकता है क्योंकि मानक पुस्तकालय का उद्देश्य विशिष्ट सामान को लागू करने के लिए एक मानक इंटरफ़ेस प्रदान करना है।
प्लाज्माएच

148

इसके लिए कुछ विवरण / पृष्ठभूमि के बारे में टिप्पणियों में बहुत (थोड़ा या पूरी तरह से) गलत अनुमान लगाया गया है।

आप 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_BIT8 का गुणक है । सभी 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 long4 या 8 बाइट्स है। या शायद यह वास्तव में unsigned long8 तक के किसी भी आकार के लिए काम करेगा , और यह 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उस लोड बैंडविड्थ के साथ रखने के लिए पर्याप्त निष्पादन थ्रूपुट है ।

बेशक प्रोग्राम जो बड़े स्ट्रिंग्स के साथ काम करते हैं, उन्हें आमतौर पर लंबाई का ट्रैक रखना चाहिए ताकि इन-लेंथ सी स्ट्रिंग्स की लंबाई का पता लगाने में बहुत कम समय लगे। लेकिन लघु से मध्यम लंबाई के प्रदर्शन अभी भी हाथ से लिखे हुए कार्यान्वयन से लाभान्वित होते हैं, और मुझे यकीन है कि कुछ कार्यक्रम मध्यम-लंबाई के स्ट्रिंग्स पर स्ट्रलेन का उपयोग करके समाप्त होते हैं।


12
कुछ नोट्स: (1) वर्तमान में जीसीसी के अलावा किसी भी संकलक के साथ खुद को ग्लिबेक संकलित करना संभव नहीं है। (2) वर्तमान में इस प्रकार के मामलों के कारण लिंक-टाइम ऑप्टिमाइज़ेशन सक्षम होने के साथ ही ग्लिबेक को स्वयं संकलित करना संभव नहीं है, जहां कंपाइलर यूबी देखेगा यदि इनलाइनिंग की अनुमति है। (3) CHAR_BIT == 8एक POSIX आवश्यकता है (-2001 रेव के रूप में; यहाँ देखें )। (4) strlenकुछ समर्थित सीपीयू के लिए C फॉलबैक कार्यान्वयन का उपयोग किया जाता है, मेरा मानना ​​है कि सबसे आम एक MIPS है।
zwol

1
दिलचस्प बात यह है कि सख्त-अलियासिंग यूबी को __attribute__((__may_alias__))विशेषता का उपयोग करके तय किया जा सकता है (यह गैर-पोर्टेबल है, लेकिन यह ग्लिबक के लिए ठीक होना चाहिए)।
कोनराड बोरोस्की

1
@SebastianRedl: आप किसी भी वस्तु को पढ़ सकते हैं / उसके माध्यम से लिख सकते हैं char*, लेकिन किसी वस्तु के माध्यम से किसी char वस्तु (जैसे का हिस्सा char[]) को पढ़ना / लिखना अभी भी यूबी है long*सख्त अलियासिंग नियम और 'चार *' पॉइंटर्स
पीटर कॉर्ड्स

1
C और C ++ मानकों का कहना है कि CHAR_BITकम से कम 8 ( C11 का qv अनुलग्नक E) होना चाहिए , इसलिए कम से कम 7-बिट charऐसा कुछ नहीं है जिसके बारे में भाषा के वकील को चिंता करने की आवश्यकता है। यह आवश्यकता से प्रेरित था, "UTF liter 8 स्ट्रिंग शाब्दिकों के लिए, सरणी तत्व टाइप होते हैं char, और मल्टीफ़ाइट वर्ण अनुक्रम के वर्णों के साथ आरंभीकृत होते हैं, जैसा कि UTF। 8 में एन्कोड किया गया है।"
डेविस्लर

2
लगता है कि यह विश्लेषण पैच का प्रस्ताव करने के लिए एक अच्छा आधार है जो वर्तमान में अक्षम आशाओं के सामने कोड को अधिक मजबूत बनाता है, एक भयानक जवाब देने से अलग।
Deduplicator

61

यह आपके द्वारा लिंक की गई फ़ाइल में टिप्पणियों में समझाया गया है:

 27 /* Return the length of the null-terminated string STR.  Scan for
 28    the null terminator quickly by testing four bytes at a time.  */

तथा:

 73   /* Instead of the traditional loop which tests each character,
 74      we will test a longword at a time.  The tricky part is testing
 75      if *any of the four* bytes in the longword in question are zero.  */

सी में, दक्षता के बारे में विस्तार से तर्क करना संभव है।

यह अलग-अलग पात्रों के माध्यम से पुनरावृत्ति करने के लिए कम कुशल है, यह एक बार में एक से अधिक बाइट का परीक्षण करने की तुलना में एक शून्य की तलाश में है, जैसा कि यह कोड करता है।

अतिरिक्त जटिलता यह सुनिश्चित करने के लिए आती है कि परीक्षण के तहत स्ट्रिंग को सही जगह पर एक समय में एक से अधिक बाइट का परीक्षण शुरू करने के लिए संरेखित किया जाता है (एक लंबी सीमा के साथ, जैसा कि टिप्पणियों में वर्णित है), और यह सुनिश्चित करने की आवश्यकता से कि धारणाएं जब कोड का उपयोग किया जाता है, तो डेटाटिप्स के आकार का उल्लंघन नहीं किया जाता है।

में सबसे (लेकिन सभी नहीं) आधुनिक सॉफ्टवेयर विकास, दक्षता विस्तार को यह ध्यान आवश्यक हो, या अतिरिक्त कोड जटिलता की लागत के लायक नहीं नहीं है।

एक जगह जहां यह दक्षता पर ध्यान देने के लिए समझ में आता है इस तरह से मानक पुस्तकालयों में है, जैसे कि आप जुड़े हुए उदाहरण।


यदि आप शब्द सीमाओं के बारे में अधिक पढ़ना चाहते हैं, तो इस प्रश्न और इस उत्कृष्ट विकिपीडिया पृष्ठ को देखें


39

यहाँ महान जवाब के अलावा, मैं यह बताना चाहता हूं कि प्रश्न में जुड़ा कोड GNU के कार्यान्वयन के लिए है strlen

की OpenBSD कार्यान्वयनstrlen बहुत सवाल में प्रस्तावित कोड के समान है। एक कार्यान्वयन की जटिलता लेखक द्वारा निर्धारित की जाती है।

...
#include <string.h>

size_t
strlen(const char *str)
{
    const char *s;

    for (s = str; *s; ++s)
        ;
    return (s - str);
}

DEF_STRONG(strlen);

संपादित करें : ऊपर मैंने जो OpenBSD कोड लिंक किया है, वह ISAs के लिए एक कार्यान्वयन कार्यान्वयन लगता है, जिसमें स्वयं का कार्यान्वयन नहीं है। strlenवास्तुकला के आधार पर अलग-अलग कार्यान्वयन हैं । उदाहरण के लिए, amd64strlen के लिए कोड asm है। पीटरकॉर्ड्स की टिप्पणियों / उत्तर के समान है जो बताता है कि गैर-फॉलबैक जीएनयू कार्यान्वयन समान हैं।


5
यह OpenBSD बनाम GNU टूल में अनुकूलित किए जा रहे विभिन्न मूल्यों का बहुत अच्छा चित्रण करता है।
जेसन

11
यह glibc का पोर्टेबल कमबैक कार्यान्वयन है। सभी प्रमुख आईएसएएस ने glibc में हाथ से लिखे गए एसिमेशन को लागू किया है, जब यह मदद करता है तो SIMD का उपयोग करता है (जैसे x86 पर)। देखें code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/... और code.woboq.org/userspace/glibc/sysdeps/aarch64/multiarch/...
पीटर Cordes

4
यहां तक ​​कि OpenBSD संस्करण में एक दोष है जो मूल से बचा जाता है! s - strयदि परिणाम में प्रतिनिधित्व योग्य नहीं है, तो व्यवहार अपरिभाषित है ptrdiff_t
अंटी हापला

1
@AnttiHaapala: GNU C में, अधिकतम ऑब्जेक्ट का आकार है PTRDIFF_MAX। लेकिन अभी भी कम mmapसे कम लिनक्स पर इससे अधिक मेमोरी संभव है (उदाहरण के लिए x86-64 कर्नेल के तहत 32-बिट प्रक्रिया में मैं विफल होने से पहले 2.7 जीबी सन्निहित के बारे में मिमीप कर सकता हूं)। OpenBSD के बारे में IDK; कर्नेल को उस तक पहुंचना असंभव बना सकता है, जो returnबिना साइगफॉल्टिंग या आकार के भीतर रुक सकता है। लेकिन हाँ, आपको लगता है कि रक्षात्मक कोडिंग जो सैद्धांतिक सी UB से बचती है, कुछ OpenBSD करना चाहेगी। हालांकि strlenइनलाइन और वास्तविक संकलक नहीं कर सकते हैं, बस इसे घटाना के लिए संकलित करेंगे।
पीटर कॉर्डेस

2
@PeterCordes बिल्कुल OpenBSD में समान बात, उदाहरण के लिए i386 असेंबली: cvsweb.openbsd.org/cgi-bin/cvsweb/src/lib/libc/arch/i386/string/…
dchest

34

संक्षेप में, यह एक प्रदर्शन अनुकूलन है जो मानक लाइब्रेरी यह जानकर कर सकती है कि इसे किस कंपाइलर के साथ संकलित किया गया है - आपको इस तरह का कोड नहीं लिखना चाहिए, जब तक आप एक मानक लाइब्रेरी नहीं लिख रहे हैं और एक विशिष्ट कंपाइलर पर निर्भर हो सकते हैं। विशेष रूप से, यह एक ही समय में बाइट्स के संरेखण संख्या को संसाधित कर रहा है - 32-बिट प्लेटफार्मों पर 4, 64-बिट प्लेटफार्मों पर 8। इसका मतलब है कि यह भोले बाइट पुनरावृत्ति की तुलना में 4 या 8 गुना तेज हो सकता है।

यह कैसे काम करता है, यह समझाने के लिए, निम्नलिखित छवि पर विचार करें। यहां 32-बिट प्लेटफॉर्म मान लें (4 बाइट्स संरेखण)।

मान लीजिए कि "हैलो", दुनिया का अक्षर "H" है! स्ट्रिंग को तर्क के रूप में प्रदान किया गया था strlen। क्योंकि सीपीयू मेमोरी में चीजों को संरेखित करना पसंद करता है (आदर्श रूप से address % sizeof(size_t) == 0), संरेखण से पहले बाइट्स को धीमी विधि का उपयोग करके बाइट-बाय-बाइट संसाधित किया जाता है।

फिर, प्रत्येक संरेखण आकार के लिए, (longbits - 0x01010101) & 0x80808080 != 0यह गणना करके कि क्या किसी पूर्णांक के भीतर बाइट्स शून्य है। कम से कम एक बाइट्स की तुलना में अधिक होने पर यह गणना एक झूठी सकारात्मक है 0x80, लेकिन अधिक बार यह काम नहीं करना चाहिए। यदि यह मामला नहीं है (जैसा कि यह पीले क्षेत्र में है), लंबाई संरेखण आकार से बढ़ जाती है।

यदि किसी पूर्णांक के भीतर का कोई बाइट शून्य (या 0x81) हो जाता है, तो शून्य की स्थिति निर्धारित करने के लिए स्ट्रिंग को बाइट-बाय-बाइट से चेक किया जाता है।

यह एक आउट-ऑफ-बाउंड एक्सेस बना सकता है, हालांकि यह एक संरेखण के भीतर होने के कारण, यह ठीक नहीं होने की संभावना है, मेमोरी मैपिंग इकाइयां आमतौर पर बाइट स्तर सटीक नहीं होती हैं।


यह कार्यान्वयन glibc का हिस्सा है। जीएनयू प्रणाली पृष्ठ सुरक्षा के साथ स्मृति संरक्षण करती है। तो हां, एक संरेखित लोड जिसमें कोई मान्य बाइट्स शामिल हैं, सुरक्षित है।
पीटर कॉर्डेस

size_tगठबंधन करने की गारंटी नहीं है।
एसएस ऐनी

32

आप चाहते हैं कि कोड सही, रखरखाव योग्य और तेज़ हो। इन कारकों का अलग महत्व है:

"सही" बिल्कुल आवश्यक है।

"बनाए रखने योग्य" इस बात पर निर्भर करता है कि आप कोड को बनाए रखने के लिए कितना जा रहे हैं: स्ट्रलेन 40 से अधिक वर्षों से एक मानक सी लाइब्रेरी फ़ंक्शन है। यह बदलने वाला नहीं है। इसलिए इस समारोह के लिए स्थिरता काफी महत्वहीन है।

"फास्ट": कई अनुप्रयोगों में, स्ट्रैची, स्ट्रलेन आदि निष्पादन समय की एक महत्वपूर्ण राशि का उपयोग करते हैं। इस जटिल के रूप में एक ही समग्र गति हासिल करने के लिए, लेकिन कंपाइलर में सुधार के द्वारा स्ट्रलेन के बहुत जटिल कार्यान्वयन के लिए वीरतापूर्ण प्रयास नहीं होंगे।

तेजी से होने का एक और फायदा है: जब प्रोग्रामर को पता चलता है कि "स्ट्रलेन" को कॉल करना सबसे तेज़ तरीका है, तो वे एक स्ट्रिंग में बाइट्स की संख्या को माप सकते हैं, उन्हें चीजों को तेज़ करने के लिए अपना कोड लिखने के लिए अब और लुभाया नहीं जाता है।

इसलिए स्ट्रैलेन के लिए, गति बहुत अधिक महत्वपूर्ण है, और बनाए रखने की क्षमता बहुत कम महत्वपूर्ण है, अधिकांश कोड के लिए जो आप कभी भी लिखेंगे।

यह इतना जटिल क्यों होना चाहिए? कहते हैं कि आपके पास 1,000 बाइट स्ट्रिंग है। सरल कार्यान्वयन 1,000 बाइट्स की जांच करेगा। वर्तमान कार्यान्वयन संभवतः 64 बिट शब्दों की एक बार में जांच करेगा, जिसका अर्थ है 125 64-बिट या आठ-बाइट शब्द। यह एक बार में 32 बाइट्स की जांच करने वाले वेक्टर निर्देशों का उपयोग भी कर सकता है, जो कि अधिक जटिल और यहां तक ​​कि तेज भी होगा। वेक्टर निर्देशों का उपयोग करने से कोड होता है जो थोड़ा अधिक जटिल होता है, लेकिन काफी सरल होता है, यह जांचना कि क्या 64 बिट शब्द में आठ बाइट्स में से एक शून्य है कुछ चतुर चाल की आवश्यकता है। तो मध्यम से लंबे तार के लिए यह कोड लगभग चार गुना तेज होने की उम्मीद की जा सकती है। किसी फ़ंक्शन के लिए जितना महत्वपूर्ण स्ट्रलेन है, उतना ही अधिक जटिल फ़ंक्शन लिखने के लिए लायक है।

पुनश्च। कोड बहुत पोर्टेबल नहीं है। लेकिन यह मानक सी लाइब्रेरी का हिस्सा है, जो कार्यान्वयन का हिस्सा है - यह पोर्टेबल होने की आवश्यकता नहीं है।

पी पी एस। किसी ने एक उदाहरण पोस्ट किया जहां एक डिबगिंग टूल ने स्ट्रिंग के अंत में बाइट्स तक पहुंचने के बारे में शिकायत की। एक कार्यान्वयन डिज़ाइन किया जा सकता है जो निम्नलिखित की गारंटी देता है: यदि p बाइट के लिए एक वैध सूचक है, तो उसी संरेखित ब्लॉक में बाइट के लिए कोई भी एक्सेस जो कि सी मानक के अनुसार अपरिभाषित व्यवहार होगा, एक अनिर्दिष्ट मान लौटाएगा।

PPPS। इंटेल ने अपने बाद के प्रोसेसर के लिए निर्देश जोड़े हैं जो स्ट्रैस () फ़ंक्शन के लिए एक बिल्डिंग ब्लॉक बनाते हैं (स्ट्रिंग में एक विकल्प खोजना)। उनका विवरण दिमाग से टकरा रहा है, लेकिन वे उस विशेष कार्य को संभवतः 100 गुना तेज कर सकते हैं। (मूल रूप से, एक सरणी जिसमें "हैलो, दुनिया!" दी गई है और एक सरणी b 16 बाइट्स "HelloHelloHelloH" से शुरू होती है और अधिक बाइट्स होती है, यह पता लगाता है कि स्ट्रिंग बी में पहले से नहीं होती है, जो सूचकांक 15 से शुरू होता है। ।


या ... अगर मुझे पता चल रहा है कि मैं बहुत सारे स्ट्रिंग आधारित प्रसंस्करण कर रहा हूं और एक अड़चन है, तो मैं शायद स्ट्रैनल को सुधारने के बजाय पास्कल स्ट्रिंग्स के अपने संस्करण को लागू करने जा रहा हूं ...
बाल्ड्रिक

1
कोई भी आपको स्ट्रगल में सुधार करने के लिए नहीं कहता है । लेकिन इसे अच्छा बनाने से लोग अपने खुद के तार को लागू करने जैसी बकवास से बचते हैं।
gnasher729


24

संक्षेप में: बाइट द्वारा एक स्ट्रिंग बाइट की जांच करना संभवतः आर्किटेक्चर पर धीमा होगा जो एक बार में बड़ी मात्रा में डेटा प्राप्त कर सकता है।

यदि शून्य समाप्ति की जांच 32 या 64 बिट के आधार पर की जा सकती है, तो यह संकलक द्वारा किए जाने वाले चेक की मात्रा को कम कर देता है। यही कारण है कि लिंक एक विशिष्ट प्रणाली को ध्यान में रखते हुए करने का प्रयास करता है। वे संबोधित करने, संरेखण, कैश उपयोग, गैर-मानक संकलक सेटअप आदि आदि के बारे में धारणा बनाते हैं।

आपके उदाहरण के रूप में बाइट द्वारा बाइट पढ़ना 8 बिट सीपीयू पर एक समझदार दृष्टिकोण होगा, या जब मानक सी में लिखा गया पोर्टेबल लिबास लिख रहा हो।

तेज / अच्छा कोड लिखने की सलाह देने के लिए सी मानक के कामों को देखना एक अच्छा विचार नहीं है, क्योंकि यह गैर-पोर्टेबल होगा और गैर-मानक मान्यताओं या खराब-परिभाषित व्यवहार पर निर्भर करेगा। यदि आप एक शुरुआत कर रहे हैं, तो इस तरह के कोड को पढ़ना शैक्षिक की तुलना में अधिक हानिकारक होगा।


1
बेशक ऑप्टिमाइज़र इस लूप को अनियंत्रित या ऑटो-वेक्टर करने की अत्यधिक संभावना है, और पूर्व-भ्रूण इस पहुंच पैटर्न का तुच्छ रूप से पता लगा सकते हैं। क्या ये चालें वास्तव में आधुनिक प्रोसेसर पर मायने रखती हैं, इसका परीक्षण करने की आवश्यकता होगी। अगर वहाँ एक जीत है यह शायद वेक्टर निर्देशों का उपयोग कर रहा है।
रुसबिशॉप

6
@rbbishop: आप ऐसा चाहते हैं, लेकिन नहीं। जीसीसी और क्लैंग ऑटो-वेक्टरिंग लूप्स में पूरी तरह से असमर्थ हैं, जहां पुनरावृत्ति गिनती पहले पुनरावृत्ति से आगे नहीं जानी जाती है। जिसमें डेटा-निर्भर के साथ खोज लूप, या कोई अन्य लूप शामिल है if()break। ICC ऐसे लूपों को ऑटो-वेक्टर कर सकता है, लेकिन IDK कितनी अच्छी तरह से एक भोली स्ट्रलेन के साथ करता है। और हाँ, SSE2 pcmpeqb/ pmovmskbहै बहुत strlen के लिए अच्छा है, एक समय में 16 बाइट्स का परीक्षण। code.woboq.org/userspace/glibc/sysdeps/x86_64/strlen.S.html glibc का SSE2 संस्करण है। इस प्रश्नोत्तर को भी देखें ।
पीटर कॉर्ड्स

उफ, यह दुर्भाग्यपूर्ण है। मैं आमतौर पर बहुत यूबी विरोधी हूं, लेकिन जैसा कि आप बताते हैं कि सी स्ट्रिंग को वैश्वीकरण की अनुमति देने के लिए तकनीकी रूप से यूबी एंड-ऑफ-बफर पढ़ने की आवश्यकता है। मुझे लगता है कि ARM64 के लिए भी यही बात लागू होती है क्योंकि इसमें संरेखण की आवश्यकता होती है।
रूसबोप

-6

अन्य उत्तरों द्वारा उल्लेखित एक महत्वपूर्ण बात यह नहीं है कि एफएसएफ यह सुनिश्चित करने के बारे में बहुत सतर्क है कि मालिकाना कोड इसे जीएनपी परियोजनाओं में नहीं बनाता है। प्रोप्रायटरी प्रोग्राम्स के संदर्भ में GNU कोडिंग मानकों में , आपके कार्यान्वयन को इस तरह से व्यवस्थित करने के बारे में एक चेतावनी है कि इसे मौजूदा स्वामित्व कोड के साथ भ्रमित नहीं किया जा सकता है:

किसी भी परिस्थिति में या GNU पर आपके काम के दौरान यूनिक्स स्रोत कोड का संदर्भ न लें! (या किसी अन्य मालिकाना कार्यक्रमों के लिए।)

यदि आपके पास एक यूनिक्स कार्यक्रम के आंतरिक का एक अस्पष्ट स्मरण है, तो इसका मतलब यह बिल्कुल नहीं है कि आप इसकी नकल नहीं लिख सकते हैं, लेकिन अलग-अलग लाइनों के साथ आंतरिक रूप से नकल को व्यवस्थित करने की कोशिश करें, क्योंकि इससे विवरण बनाने की संभावना है यूनिक्स संस्करण अप्रासंगिक और आपके परिणामों से भिन्न है।

उदाहरण के लिए, यूनिक्स उपयोगिताओं को आमतौर पर मेमोरी उपयोग को कम करने के लिए अनुकूलित किया गया था; यदि आप इसके बजाय गति के लिए जाते हैं , तो आपका कार्यक्रम बहुत अलग होगा।

(जोर मेरा)


5
यह प्रश्न का उत्तर कैसे देता है?
एसएस ऐनी

1
ओपी में सवाल यह था कि "क्या यह सरल कोड बेहतर काम नहीं करेगा?" GNU जैसी परियोजना के लिए, कानूनी नुकसान से बचने के लिए कोड का एक महत्वपूर्ण हिस्सा "बेहतर काम करना" है, और "स्पष्ट" कार्यान्वयन strlen()मौजूदा कोड के समान या समान होने की संभावना है। Glibc के कार्यान्वयन के रूप में "पागल" के रूप में कुछ इस तरह वापस पता नहीं लगाया जा सकता है। rangeCheckकोड की - 11 पंक्तियों पर कितना कानूनी तकरार था यह देखते हुए ! - Google / Oracle लड़ाई में, मैं कहूंगा कि FSF की चिंता अच्छी तरह से रखी गई थी।
जैक केली
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.