Tại sao strlen của glibc cần phải quá phức tạp để chạy nhanh?


286

Tôi đã xem qua strlenở đây và tôi đã tự hỏi nếu tối ưu hóa được sử dụng trong mã là thực sự cần thiết? Ví dụ, tại sao những thứ như sau lại hoạt động tốt như nhau hoặc tốt hơn?

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

Không phải mã đơn giản tốt hơn và / hoặc dễ dàng hơn cho trình biên dịch để tối ưu hóa?

Mã của strlentrang phía sau liên kết trông như thế này:

/* 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)

Tại sao phiên bản này chạy nhanh?

Nó không làm nhiều việc không cần thiết sao?


2
Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
Samuel Liew

18
Để tham khảo trong tương lai, kho lưu trữ nguồn chính thức cho GNU libc có tại < sourceware.org/git/?p=glibc.git >. < sourceware.org/git/?p=glibc.git;a=blob;f=opes/ Khăn > thực sự hiển thị mã tương tự như trên; tuy nhiên, sysdepsthay vào đó, việc sử dụng ngôn ngữ lắp ráp viết tay từ thư mục sẽ được sử dụng, trên hầu hết các kiến ​​trúc được hỗ trợ của glibc (kiến trúc được sử dụng phổ biến nhất không có thay thế là MIPS).
zwol

9
Bỏ phiếu để đóng này là chủ yếu dựa trên ý kiến; "Có phải xxx thực sự cần thiết trong xxx?" là chủ quan với ý kiến ​​của mọi người.
SS Anne

2
@ JL2210: Điểm hay, đã sửa tiêu đề để nắm bắt tinh thần của câu hỏi trong một tiêu đề nghe có vẻ không cần thiết nếu hiệu suất là cần thiết, tại sao chúng ta cần những tối ưu hóa này để có hiệu suất.
Peter Cordes

9
@ JL2210 FWIW, tiêu đề ban đầu là "Tại sao strlen lại phức tạp trong C [sic!]", Và nó đã bị đóng là "quá rộng", sau đó mở lại, sau đó đóng lại là "chủ yếu dựa trên quan điểm". Tôi đã cố gắng khắc phục điều này (vướng vào "bạn đã phá vỡ câu hỏi của tôi!" Và "các bạn đang lạm dụng quyền hạn chỉnh sửa của bạn!" Trong khi đó), nhưng IMVHO đã nói dối vấn đề (và vẫn là nói dối) trong tiền đề cơ bản của câu hỏi, đó là vấn đề ("mã này quá phức tạp đối với tôi để hiểu" không phù hợp cho Hỏi & Đáp - IMO đó là một yêu cầu dạy kèm, không phải để trả lời). Tôi sẽ không chạm vào nó một lần nữa với cột 60 feet :)

Câu trả lời:


233

Bạn không cần và bạn không bao giờ nên viết mã như vậy - đặc biệt nếu bạn không phải là nhà cung cấp thư viện C / trình biên dịch chuẩn. Đó là mã được sử dụng để thực hiện strlenvới một số hack và giả định tốc độ rất đáng nghi ngờ (không được kiểm tra với các xác nhận hoặc được đề cập trong các bình luận):

  • unsigned long là 4 hoặc 8 byte
  • byte là 8 bit
  • một con trỏ có thể được truyền tới unsigned long longvà khônguintptr_t
  • người ta có thể căn chỉnh con trỏ đơn giản bằng cách kiểm tra xem 2 hoặc 3 bit thứ tự thấp nhất bằng không
  • người ta có thể truy cập một chuỗi như unsigned longs
  • người ta có thể đọc qua phần cuối của mảng mà không có bất kỳ hiệu ứng xấu nào.

Hơn nữa, một trình biên dịch tốt thậm chí có thể thay thế mã được viết là

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

(lưu ý rằng nó phải là một loại tương thích với size_t ) với một phiên bản nội tuyến của trình biên dịch dựng sẵn strlen, hoặc vector hóa mã; nhưng một trình biên dịch sẽ khó có thể tối ưu hóa phiên bản phức tạp.


Các strlenchức năng được mô tả bằng C11 7.24.6.3 như:

Sự miêu tả

  1. Các strlen chức năng tính độ dài của chuỗi được trỏ đến bởi s.

Trả về

  1. Các strlenchức năng trả về số ký tự trước các nhân vật chấm dứt null.

Bây giờ, nếu chuỗi được trỏ đến slà trong một mảng các ký tự đủ dài để chứa chuỗi và NUL kết thúc, thì hành vi sẽ không được xác định nếu chúng ta truy cập chuỗi qua bộ kết thúc null, ví dụ như trong

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

Vì vậy, cách thực sự duy nhất trong C hoàn toàn di động / tuân thủ tiêu chuẩn C để thực hiện điều này một cách chính xác là cách nó được viết trong câu hỏi của bạn , ngoại trừ các biến đổi tầm thường - bạn có thể giả vờ nhanh hơn bằng cách hủy vòng lặp, v.v. một byte mỗi lần.

(Như các nhà bình luận đã chỉ ra, khi tính di động nghiêm ngặt là quá nhiều gánh nặng, việc tận dụng các giả định hợp lý hoặc an toàn không phải lúc nào cũng là một điều xấu. Đặc biệt trong mã đó là một phần của việc thực hiện C cụ thể. Nhưng bạn phải hiểu quy tắc trước khi biết làm thế nào / khi bạn có thể uốn cong chúng.)


Việc strlenthực hiện được liên kết trước tiên sẽ kiểm tra các byte riêng lẻ cho đến khi con trỏ trỏ đến ranh giới căn chỉnh 4 hoặc 8 byte tự nhiên của unsigned long. Tiêu chuẩn C nói rằng việc truy cập một con trỏ không được căn chỉnh chính xác có hành vi không xác định , do đó, điều này hoàn toàn phải được thực hiện để thủ thuật bẩn tiếp theo thậm chí còn bẩn hơn. (Trong thực tế trên một số kiến ​​trúc CPU khác với x86, một từ bị tải sai hoặc tải từ kép sẽ bị lỗi. C là không phải là ngôn ngữ lắp ráp di động, nhưng mã này đang sử dụng theo cách đó). Đó cũng là điều giúp bạn có thể đọc qua phần cuối của một đối tượng mà không có nguy cơ bị lỗi khi triển khai trong đó bảo vệ bộ nhớ hoạt động trong các khối được căn chỉnh (ví dụ: các trang bộ nhớ ảo 4kiB).

Bây giờ đến phần bẩn: mã phá vỡ lời hứa và đọc 4 hoặc 8 byte 8 bit tại một thời điểm (a long int) và sử dụng một mẹo nhỏ với phép cộng không dấu để nhanh chóng tìm ra nếu có bất kỳ byte 0 nào trong 4 hoặc 8 byte đó byte - nó sử dụng một số được chế tạo đặc biệt để làm cho bit carry thay đổi các bit bị bắt bởi mặt nạ bit. Về bản chất, điều này sau đó sẽ tìm ra liệu có bất kỳ 4 hoặc 8 byte nào trong mặt nạ là các số 0 được cho là nhanh hơn so với việc lặp qua từng byte này không. Cuối cùng, có một vòng lặp ở cuối để tìm ra byte là zero đầu tiên, nếu có, và trả về kết quả.

Vấn đề lớn nhất là trong sizeof (unsigned long) - 1các sizeof (unsigned long)trường hợp trong trường hợp, nó sẽ đọc qua phần cuối của chuỗi - chỉ khi byte null nằm trong byte được truy cập cuối cùng (nghĩa là ở phần cuối nhỏ nhất có ý nghĩa nhất và ở phần cuối lớn nhất là ít quan trọng nhất) , nó không truy cập vào các mảng ngoài giới hạn!


Mã, mặc dù được sử dụng để thực hiện strlentrong thư viện chuẩn C làxấu . Nó có một số khía cạnh được xác định thực hiện và không xác định trong đó và nó không nên được sử dụng ở bất cứ đâu thay vì hệ thống được cung cấp strlen- Tôi đã đổi tên hàm thành the_strlenở đây và thêm vào như sau main:

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

Bộ đệm có kích thước cẩn thận để có thể chứa chính xác hello worldchuỗi và bộ kết thúc. Tuy nhiên, trên bộ xử lý 64 bit của tôi unsigned longlà 8 byte, vì vậy quyền truy cập vào phần sau sẽ vượt quá bộ đệm này.

Nếu bây giờ tôi biên dịch với -fsanitize=undefined-fsanitize=addressvà chạy chương trình kết quả, tôi nhận được:

% ./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

tức là những điều tồi tệ đã xảy ra


120
Re: "giả định tốc độ rất đáng nghi ngờ và giả định" - đó là, rất đáng nghi ngờ trong mã di động . Thư viện chuẩn được viết cho một tổ hợp trình biên dịch / phần cứng cụ thể, với kiến ​​thức về hành vi thực tế của những thứ mà định nghĩa ngôn ngữ để lại là không xác định. Đúng, hầu hết mọi người không nên viết mã như vậy, nhưng trong bối cảnh triển khai thư viện tiêu chuẩn không di động thì vốn không tệ.
Pete Becker

4
Đồng ý, đừng bao giờ tự viết những thứ như thế này. Hoặc gần như không bao giờ. Tối ưu hóa sớm là nguồn gốc của mọi tội lỗi. (Trong trường hợp này nó thực sự có thể được thúc đẩy). Nếu bạn kết thúc thực hiện nhiều lệnh gọi strlen () trên cùng một chuỗi rất dài, ứng dụng của bạn có thể được viết khác đi. Bạn di chuyển như ví dụ lưu chuỗi chuỗi trong một biến đã có khi chuỗi được tạo và hoàn toàn không cần gọi strlen ().
ghellquist

65
@ghellquist: Tối ưu hóa cuộc gọi thư viện thường được sử dụng hầu như không phải là "tối ưu hóa sớm".
jamesqf

7
@Antti Haapala: Chính xác thì tại sao bạn nghĩ strlen nên là O (1)? Và những gì chúng ta có ở đây là một số triển khai, tất cả đều là O (n), nhưng với các bội số không đổi khác nhau. Bạn có thể không nghĩ đó là vấn đề, nhưng đối với một số người trong chúng ta, việc triển khai thuật toán O (n) hoạt động trong vài giây là tốt hơn nhiều so với mất vài giây, thậm chí là mili giây, bởi vì nó có thể được gọi là vài tỷ lần trong Tất nhiên của một công việc.
jamesqf

8
@PeteBecker: không chỉ vậy, trong bối cảnh các thư viện tiêu chuẩn (không quá nhiều trong trường hợp này), viết mã không thể truy cập có thể là tiêu chuẩn vì mục đích của thư viện chuẩn là cung cấp giao diện chuẩn cho việc thực hiện cụ thể.
PlasmaHH

148

Có rất nhiều dự đoán sai (hơi hoặc hoàn toàn) trong các nhận xét về một số chi tiết / bối cảnh cho việc này.

Bạn đang xem triển khai dự phòng tối ưu hóa C được tối ưu hóa của glibc. (Đối với các ISA không có triển khai asm viết tay) . Hoặc một phiên bản cũ của mã đó, vẫn còn trong cây nguồn glibc. https://code.woboq.org/userspace/glibc/opes/strlen.c.html là một trình duyệt mã dựa trên cây git glibc hiện tại. Rõ ràng nó vẫn được sử dụng bởi một vài mục tiêu glibc chính thống, bao gồm MIPS. (Cảm ơn @zwol).

Trên các ISA phổ biến như x86 và ARM, glibc sử dụng mã asm viết tay

Vì vậy, khuyến khích thay đổi bất cứ điều gì về mã này thấp hơn bạn nghĩ.

Mã bithack này ( https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord ) không phải là những gì thực sự chạy trên máy chủ / máy tính để bàn / máy tính xách tay / điện thoại thông minh của bạn. Nó tốt hơn một vòng lặp byte ngây thơ, nhưng ngay cả bithack này cũng khá tệ so với asm hiệu quả cho các CPU hiện đại (đặc biệt là x86 trong đó AVX2 SIMD cho phép kiểm tra 32 byte với một vài hướng dẫn, cho phép 32 đến 64 byte mỗi đồng hồ chu kỳ trong vòng lặp chính nếu dữ liệu nóng trong bộ đệm L1d trên các CPU hiện đại với tải vectơ 2 / xung nhịp và thông lượng ALU. tức là đối với các chuỗi có kích thước trung bình trong đó chi phí khởi động không chiếm ưu thế.)

glibc sử dụng các thủ thuật liên kết động để phân giải strlenthành phiên bản tối ưu cho CPU của bạn, do đó, ngay cả trong x86 cũng có phiên bản SSE2 (vectơ 16 byte, đường cơ sở cho x86-64) và phiên bản AVX2 (vectơ 32 byte).

x86 có khả năng truyền dữ liệu hiệu quả giữa các thanh ghi vectơ và mục đích chung, điều này giúp cho việc sử dụng SIMD duy nhất để tăng tốc các chức năng trên các chuỗi có độ dài ẩn trong đó điều khiển vòng lặp phụ thuộc vào dữ liệu. pcmpeqb/ pmovmskblàm cho nó có thể kiểm tra 16 byte riêng biệt cùng một lúc.

glibc có phiên bản AArch64 giống như sử dụng AdvSIMD và phiên bản dành cho CPU AArch64 trong đó vectơ-> GP đăng ký đường ống, do đó, nó thực sự sử dụng bithack này . Nhưng sử dụng các số 0 đứng đầu để tìm thanh ghi byte trong khi nó bị tấn công và tận dụng các truy cập không được phân bổ hiệu quả của AArch64 sau khi kiểm tra việc vượt qua trang.

Cũng liên quan: Tại sao mã này chậm hơn 6,5 lần với tối ưu hóa được bật? có thêm một số chi tiết về những gì nhanh so với chậm trong x86 asm strlenvới một bộ đệm lớn và việc triển khai asm đơn giản có thể tốt cho gcc để biết cách nội tuyến. (Một số phiên bản gcc không chính xác nội tuyến rep scasbrất chậm hoặc bithack 4 byte một lần như thế này. Vì vậy, công thức nội tuyến strlen của GCC cần cập nhật hoặc vô hiệu hóa.)

Asm không có "hành vi không xác định" kiểu C ; Việc truy cập byte trong bộ nhớ theo cách bạn muốn là an toàn và tải được căn chỉnh bao gồm bất kỳ byte hợp lệ nào cũng không thể bị lỗi. Bảo vệ bộ nhớ xảy ra với độ chi tiết của trang được căn chỉnh; truy cập được căn chỉnh hẹp hơn mà không thể vượt qua một ranh giới trang. Có an toàn khi đọc qua phần cuối của bộ đệm trong cùng một trang trên x86 và x64 không? Lý do tương tự áp dụng cho mã máy mà bản hack C này có được trình biên dịch để tạo ra cho việc triển khai phi tuyến tính độc lập của chức năng này.

Khi một trình biên dịch phát ra mã để gọi một hàm không nội tuyến không xác định, nó phải giả sử rằng hàm đó sửa đổi bất kỳ / tất cả các biến toàn cục và bất kỳ bộ nhớ nào mà nó có thể có một con trỏ tới. tức là mọi thứ trừ người dân địa phương không có địa chỉ thoát đều phải được đồng bộ hóa trong bộ nhớ trong suốt cuộc gọi. Điều này áp dụng cho các chức năng được viết bằng asm, rõ ràng, nhưng cũng cho các chức năng thư viện. Nếu bạn không kích hoạt tối ưu hóa thời gian liên kết, nó thậm chí còn áp dụng cho các đơn vị dịch thuật riêng biệt (tệp nguồn).


Tại sao điều này là an toàn như là một phần của glibc nhưng không phải là khác.

Yếu tố quan trọng nhất là điều này strlenkhông thể nội tuyến vào bất cứ điều gì khác. Nó không an toàn cho điều đó; nó chứa UB khử răng cưa nghiêm ngặt (đọc chardữ liệu thông qua một unsigned long*). char*được phép bí danh bất cứ điều gì khác nhưng điều ngược lại là không đúng sự thật .

Đây là một chức năng thư viện cho một thư viện được biên dịch trước (glibc). Nó sẽ không được kết nối với tối ưu hóa thời gian liên kết vào người gọi. Điều này có nghĩa là nó chỉ phải biên dịch thành mã máy an toàn cho phiên bản độc lập strlen. Nó không phải là di động / an toàn C.

Thư viện GNU C chỉ phải biên dịch với GCC. Rõ ràng nó không được hỗ trợ để biên dịch nó bằng tiếng kêu hoặc ICC, mặc dù chúng hỗ trợ các phần mở rộng GNU. GCC là trình biên dịch trước thời hạn biến tệp nguồn C thành tệp đối tượng của mã máy. Không phải là trình thông dịch, vì vậy trừ khi nó nội tuyến vào thời gian biên dịch, các byte trong bộ nhớ chỉ là các byte trong bộ nhớ. tức là UB răng cưa nghiêm ngặt không nguy hiểm khi các truy cập với các loại khác nhau xảy ra trong các chức năng khác nhau không liên kết với nhau.

Hãy nhớ rằng strlenhành vi của nó được xác định bởi tiêu chuẩn ISO C. Tên chức năng cụ thể là một phần của việc thực hiện. Các trình biên dịch như GCC thậm chí coi tên là hàm tích hợp trừ khi bạn sử dụng -fno-builtin-strlen, do đó strlen("foo")có thể là hằng số thời gian biên dịch 3. Định nghĩa trong thư viện chỉ được sử dụng khi gcc quyết định thực sự phát ra một cuộc gọi đến nó thay vì nội tuyến công thức riêng của mình hoặc một cái gì đó.

Khi UB không hiển thị với trình biên dịch tại thời điểm biên dịch, bạn sẽ nhận được mã máy lành mạnh. Mã máy phải hoạt động cho trường hợp không có UB và ngay cả khi bạn muốn , không có cách nào để asm phát hiện loại người gọi đã sử dụng để đưa dữ liệu vào bộ nhớ trỏ.

Glibc được biên dịch thành một thư viện tĩnh hoặc động độc lập không thể nội tuyến với tối ưu hóa thời gian liên kết. Các tập lệnh xây dựng của glibc không tạo các thư viện tĩnh "béo" chứa mã máy + gcc Biểu diễn bên trong GIMPLE để tối ưu hóa thời gian liên kết khi đưa vào chương trình. (tức là libc.asẽ không tham gia -fltotối ưu hóa thời gian liên kết vào chương trình chính.) Xây dựng glibc theo cách đó sẽ có khả năng không an toàn trên các mục tiêu thực sự sử dụng điều này.c .

Trong thực tế như bình luận @zwol, LTO không thể được sử dụng khi xây dựng glibc bản thân , vì "giòn" mã như thế này mà có thể phá vỡ nếu nội tuyến giữa file nguồn glibc là có thể. (Có một số sử dụng nội bộ strlen, ví dụ có thể là một phần của việc printftriển khai)


Điều này strlenlàm cho một số giả định:

  • CHAR_BITlà bội số của 8 . Đúng trên tất cả các hệ thống GNU. POSIX 2001 thậm chí còn đảm bảo CHAR_BIT == 8. (Điều này có vẻ an toàn cho các hệ thống có CHAR_BIT= 16hoặc 32, giống như một số DSP; vòng lặp prologue không được phân bổ sẽ luôn chạy 0 lần lặp nếu sizeof(long) = sizeof(char) = 1vì mọi con trỏ luôn được căn chỉnh và p & sizeof(long)-1luôn bằng không.) Nhưng nếu bạn có bộ ký tự không phải ASCII trong đó ký tự là 9 hoặc rộng 12 bit, 0x8080...là mẫu sai.
  • (có thể) unsigned longlà 4 hoặc 8 byte. Hoặc có thể nó thực sự sẽ hoạt động với bất kỳ kích thước nào unsigned longlên tới 8 và nó sử dụng một assert()để kiểm tra điều đó.

Hai cái đó không thể là UB, chúng chỉ là không thể di chuyển được đối với một số triển khai C. Mã này là (hoặc là) một phần của việc triển khai C trên các nền tảng nơi nó hoạt động, vì vậy điều đó tốt.

Giả định tiếp theo là tiềm năng C UB:

  • Tải được căn chỉnh có chứa bất kỳ byte hợp lệ nào cũng không thể bị lỗi và an toàn miễn là bạn bỏ qua các byte bên ngoài đối tượng bạn thực sự muốn. (Đúng như asm trên mọi hệ thống GNU và trên tất cả các CPU thông thường vì bảo vệ bộ nhớ xảy ra với độ chi tiết của trang được căn chỉnh. Có an toàn khi đọc qua phần cuối của bộ đệm trong cùng một trang trên x86 và x64? An toàn trong C khi UB không thể nhìn thấy tại thời gian biên dịch. Không có nội tuyến, đây là trường hợp ở đây. Trình biên dịch không thể chứng minh rằng đọc qua đầu tiên 0là UB, ví dụ , nó có thể là một char[]mảng C chứa {1,2,0,3})

Điểm cuối cùng đó là những gì làm cho nó an toàn khi đọc qua phần cuối của một đối tượng C ở đây. Điều đó khá an toàn ngay cả khi nội tuyến với các trình biên dịch hiện tại bởi vì tôi nghĩ rằng hiện tại họ không coi việc ngụ ý đường dẫn thực thi là không thể truy cập được. Nhưng dù sao, bí danh nghiêm ngặt đã là một showstopper nếu bạn để nội tuyến này.

Sau đó, bạn sẽ gặp các vấn đề như memcpy macro CPP không an toàn cũ của nhân Linux đã sử dụng tính năng truyền con trỏ tới unsigned long( gcc, bí danh nghiêm ngặt và các câu chuyện kinh dị ).

Điều này strlenbắt nguồn từ thời đại mà bạn có thể thoát khỏi những thứ như thế nói chung ; nó được sử dụng khá an toàn mà không cần cảnh báo "chỉ khi không nội tuyến" trước GCC3.


UB chỉ hiển thị khi nhìn qua ranh giới cuộc gọi / giữ lại không thể làm tổn thương chúng tôi. (ví dụ: gọi điều này trên một char buf[]thay vì trên một mảng của unsigned long[]cast đến a const char*). Khi mã máy được đặt thành đá, nó chỉ xử lý các byte trong bộ nhớ. Một cuộc gọi chức năng phi tuyến phải giả định rằng callee đọc bất kỳ / tất cả bộ nhớ.


Viết cái này một cách an toàn, không có răng cưa nghiêm ngặt

Các thuộc tính type GCCmay_alias đưa ra một loại cùng điều trị alias-bất cứ điều gì như char*. (Được đề xuất bởi @KonradBorowsk). Các tiêu đề GCC hiện đang sử dụng nó cho các loại vectơ SIMD x86 như __m128ivậy để bạn luôn có thể thực hiện một cách an toàn _mm_loadu_si128( (__m128i*)foo ). (Xem `` reinterpret_cast`ing giữa con trỏ vectơ phần cứng và loại tương ứng là hành vi không xác định? Để biết thêm chi tiết về ý nghĩa của việc này và không có nghĩa.)

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
     ...
  }
}

Bạn cũng có thể sử dụng aligned(1)để thể hiện một loại với alignof(T) = 1.
typedef unsigned long __attribute__((may_alias, aligned(1))) unaligned_aliasing_ulong;

Một cách di động để thể hiện tải trọng răng cưa trong ISOmemcpy , với các trình biên dịch hiện đại biết cách sắp xếp nội tuyến như một lệnh tải đơn. ví dụ

   unsigned long longword;
   memcpy(&longword, char_ptr, sizeof(longword));
   char_ptr += sizeof(longword);

Điều này cũng hoạt động đối với các tải không được phân bổ bởi vì memcpyhoạt động như là chartruy cập theo thời gian. Nhưng trong thực tế trình biên dịch hiện đại hiểu memcpyrất rõ.

Điều nguy hiểm ở đây là nếu GCC không biết chắc chắn đó char_ptrlà liên kết từ, thì nó sẽ không nội tuyến trên một số nền tảng có thể không hỗ trợ tải không được phân bổ trong asm. ví dụ MIPS trước MIPS64r6 hoặc ARM cũ hơn. Nếu bạn nhận được một hàm gọi thực tế memcpychỉ để tải một từ (và để nó trong bộ nhớ khác), đó sẽ là một thảm họa. GCC đôi khi có thể nhìn thấy khi mã sắp xếp một con trỏ. Hoặc sau vòng lặp char-at-a-time đạt đến ranh giới ulong bạn có thể sử dụng
p = __builtin_assume_aligned(p, sizeof(unsigned long));

Điều này không tránh được UB đọc quá khứ có thể, nhưng với GCC hiện tại không nguy hiểm trong thực tế.


Tại sao nguồn C được tối ưu hóa bằng tay là cần thiết: trình biên dịch hiện tại không đủ tốt

Asm được tối ưu hóa bằng tay có thể còn tốt hơn nữa khi bạn muốn mỗi lần giảm hiệu suất cuối cùng cho một chức năng thư viện tiêu chuẩn được sử dụng rộng rãi. Đặc biệt là cho một cái gì đó như memcpy, nhưng cũng có strlen. Trong trường hợp này, việc sử dụng C với nội tại x86 sẽ dễ dàng hơn nhiều để tận dụng lợi thế của SSE2.

Nhưng ở đây, chúng ta chỉ nói về một phiên bản C ngây thơ so với bithack C mà không có bất kỳ tính năng cụ thể nào của ISA.

(Tôi nghĩ rằng chúng tôi có thể mang nó như một cho rằng strlenlà rộng rãi đủ sử dụng mà làm cho nó chạy càng nhanh càng tốt là rất quan trọng. Vì vậy, câu hỏi trở nên cho dù chúng ta có thể lấy mã máy hiệu quả từ nguồn đơn giản hơn. Không, chúng tôi không thể.)

GCC và clang hiện tại không có khả năng tự động véc tơ hóa trong đó số lần lặp không được biết trước lần lặp đầu tiên . (ví dụ: phải kiểm tra xem vòng lặp có chạy ít nhất 16 lần lặp trước khi chạy lần lặp đầu tiên không.) ví dụ: tự động ghi nhớ memcpy là có thể (bộ đệm có độ dài rõ ràng) nhưng không phải là strcpy hoặc strlen (chuỗi có độ dài ẩn) trình biên dịch.

Điều đó bao gồm các vòng tìm kiếm hoặc bất kỳ vòng lặp nào khác có phụ thuộc dữ liệu if()breakcũng như bộ đếm.

ICC (trình biên dịch của Intel cho x86) có thể tự động vectơ hóa một số vòng lặp tìm kiếm, nhưng vẫn chỉ tạo ra một byte tạm thời ngây thơ cho một C đơn giản / ngây thơ strlennhư sử dụng libc của OpenBSD. ( Thần thánh ). (Từ câu trả lời của @ Peske ).

Một libc strlenđược tối ưu hóa bằng tay là cần thiết để thực hiện với các trình biên dịch hiện tại . Sử dụng 1 byte mỗi lần (với việc không kiểm soát có thể 2 byte mỗi chu kỳ trên các CPU siêu phẳng rộng) là thảm hại khi bộ nhớ chính có thể theo kịp khoảng 8 byte mỗi chu kỳ và bộ đệm L1d có thể cung cấp 16 đến 64 mỗi chu kỳ. (2x tải 32 byte mỗi chu kỳ trên CPU x86 chính hiện đại kể từ Haswell và Ryzen. Không tính AVX512 có thể giảm tốc độ xung nhịp chỉ bằng cách sử dụng vectơ 512 bit, đó là lý do tại sao glibc có thể không vội vàng thêm phiên bản AVX512 . Mặc dù với các vectơ 256 bit, AVX512VL + BW bị che khuất so sánh với mặt nạ và ktesthoặc kortestcó thể làm cho việc strlensiêu phân luồng trở nên thân thiện hơn bằng cách giảm các lần lặp / lặp của nó.)

Tôi bao gồm cả không phải x86 ở đây, đó là "16 byte". ví dụ, hầu hết các CPU AArch64 có thể làm ít nhất là điều đó, tôi nghĩ, và một số chắc chắn là nhiều hơn thế. Và một số có đủ thông lượng thực hiện strlenđể theo kịp băng thông tải đó.

Tất nhiên các chương trình hoạt động với các chuỗi lớn thường phải theo dõi độ dài để tránh phải làm lại việc tìm độ dài của chuỗi C có độ dài ẩn rất thường xuyên. Nhưng hiệu suất ngắn đến trung bình vẫn có lợi từ việc triển khai viết tay và tôi chắc chắn rằng một số chương trình cuối cùng sử dụng strlen trên chuỗi có độ dài trung bình.


12
Một vài lưu ý: (1) Hiện tại không thể tự biên dịch glibc với bất kỳ trình biên dịch nào ngoài GCC. (2) Hiện tại không thể tự biên dịch glibc với tối ưu hóa thời gian liên kết được bật, vì chính xác các loại trường hợp này, trong đó trình biên dịch sẽ thấy UB nếu nội tuyến được phép xảy ra. (3) CHAR_BIT == 8là một yêu cầu POSIX (kể từ phiên bản -2001; xem tại đây ). (4) Việc triển khai dự phòng C strlenđược sử dụng cho một số CPU được hỗ trợ, tôi tin rằng phổ biến nhất là MIPS.
zwol

1
Thật thú vị, UB khử răng cưa nghiêm ngặt có thể được sửa bằng cách sử dụng __attribute__((__may_alias__))thuộc tính (cái này không mang tính di động, nhưng nó sẽ ổn cho glibc).
Konrad Borowski

1
@SebastianRedl: Bạn có thể đọc / ghi bất kỳ đối tượng nào thông qua a char*, nhưng vẫn là UB để đọc / ghi char đối tượng (ví dụ như một phần của a char[]) thông qua a long*. Quy tắc răng cưa nghiêm ngặt và con trỏ 'char *'
Peter Cordes

1
Các tiêu chuẩn C và C ++ nói rằng CHAR_BITphải có ít nhất 8 ( qv Phụ lục E của C11), vì vậy ít nhất 7 bit charkhông phải là điều mà một luật sư ngôn ngữ cần phải lo lắng. Điều này được thúc đẩy bởi yêu cầu, chuỗi chữ ký cho UTF − 8 chuỗi, các phần tử mảng có kiểu charvà được khởi tạo với các ký tự của chuỗi ký tự đa dòng, như được mã hóa trong UTF − 8.
Davislor

2
Có vẻ như phân tích này là một cơ sở tốt để đề xuất một bản vá làm cho mã mạnh mẽ hơn khi đối mặt với các tối ưu hóa hiện đang bị vô hiệu hóa, ngoài việc đưa ra một câu trả lời tuyệt vời.
Ded repeatator

61

Nó được giải thích trong các ý kiến ​​trong tập tin bạn liên kết:

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

và:

 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.  */

Trong C, có thể suy luận chi tiết về hiệu quả.

Việc lặp lại thông qua các ký tự riêng lẻ tìm kiếm null sẽ kém hiệu quả hơn là kiểm tra nhiều hơn một byte mỗi lần, như mã này thực hiện.

Sự phức tạp bổ sung xuất phát từ việc cần phải đảm bảo rằng chuỗi được kiểm tra được căn chỉnh đúng nơi để bắt đầu kiểm tra nhiều hơn một byte mỗi lần (dọc theo một ranh giới từ dài, như được mô tả trong các nhận xét) và từ việc cần phải đảm bảo rằng các giả định về kích thước của các kiểu dữ liệu không bị vi phạm khi sử dụng mã.

Trong hầu hết (nhưng không phải tất cả) phát triển phần mềm hiện đại, sự chú ý đến chi tiết hiệu quả này là không cần thiết hoặc không đáng giá cho sự phức tạp của mã bổ sung.

Một nơi có ý nghĩa để chú ý đến hiệu quả như thế này là trong các thư viện tiêu chuẩn, giống như ví dụ bạn đã liên kết.


Nếu bạn muốn đọc thêm về ranh giới từ, xem câu hỏi này , và trang wikipedia tuyệt vời này


39

Ngoài các câu trả lời hay ở đây, tôi muốn chỉ ra rằng mã được liên kết trong câu hỏi là để triển khai GNU strlen .

Việc triển khai OpenBSDstrlen rất giống với mã được đề xuất trong câu hỏi. Sự phức tạp của một thực hiện được xác định bởi tác giả.

...
#include <string.h>

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

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

DEF_STRONG(strlen);

EDIT : Mã OpenBSD mà tôi đã liên kết ở trên có vẻ là một triển khai dự phòng cho các ISA không có triển khai asm riêng. Có các triển khai khác nhau strlentùy thuộc vào kiến ​​trúc. Mã cho amd64strlen , ví dụ, là asm. Tương tự như các nhận xét / câu trả lời của PeterCordes chỉ ra rằng các triển khai GNU không dự phòng cũng là asm.


5
Điều đó làm cho một minh họa rất hay về các giá trị khác nhau được tối ưu hóa trong các công cụ OpenBSD và GNU.
Jason

11
Đó là triển khai dự phòng di động của glibc . Tất cả các ISA chính đều có các triển khai asm viết tay trong glibc, sử dụng SIMD khi nó giúp (ví dụ trên x86). Xem code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/...code.woboq.org/userspace/glibc/sysdeps/aarch64/multiarch/...
Peter Cordes

4
Ngay cả phiên bản OpenBSD cũng có một lỗ hổng mà bản gốc tránh được! Hành vi của s - strkhông được xác định nếu kết quả không thể biểu thị trong ptrdiff_t.
Antti Haapala

1
@AnttiHaapala: Trong GNU C, kích thước đối tượng tối đa là PTRDIFF_MAX. Nhưng ít nhất vẫn có thể có mmapnhiều bộ nhớ hơn trên Linux (ví dụ: trong quy trình 32 bit dưới nhân x86-64, tôi có thể kiểm tra khoảng 2,7 GB liền kề trước khi bắt đầu gặp lỗi). IDK về OpenBSD; hạt nhân có thể làm cho nó không thể đạt được điều đó returnmà không tách biệt hoặc dừng trong kích thước. Nhưng vâng, bạn nghĩ rằng mã hóa phòng thủ tránh C UB lý thuyết sẽ là điều OpenBSD muốn làm. Mặc dù strlencác trình biên dịch nội tuyến và thực không thể biên dịch nó thành một phép trừ.
Peter Cordes

2
@PeterCordes chính xác. Điều tương tự trong OpenBSD, ví dụ như i386 lắp ráp: cvsweb.openbsd.org/cgi-bin/cvsweb/src/lib/libc/arch/i386/string/...
dchest

34

Nói tóm lại, đây là tối ưu hóa hiệu suất mà thư viện chuẩn có thể thực hiện bằng cách biết trình biên dịch được biên dịch với cái gì - bạn không nên viết mã như thế này, trừ khi bạn đang viết thư viện chuẩn và có thể phụ thuộc vào trình biên dịch cụ thể. Cụ thể, đó là xử lý số byte liên kết cùng một lúc - 4 trên nền tảng 32 bit, 8 trên nền tảng 64 bit. Điều này có nghĩa là nó có thể nhanh hơn 4 hoặc 8 lần so với phép lặp byte ngây thơ.

Để giải thích cách thức hoạt động của nó, hãy xem xét hình ảnh sau đây. Giả sử nền tảng 32 bit ở đây (căn chỉnh 4 byte).

Hãy nói rằng chữ "H" của "Xin chào, thế giới!" chuỗi được cung cấp như là một đối số cho strlen. Vì CPU thích có những thứ được căn chỉnh trong bộ nhớ (lý tưởng nhất address % sizeof(size_t) == 0), nên các byte trước khi căn chỉnh được xử lý theo từng byte, sử dụng phương thức chậm.

Sau đó, đối với mỗi khối có kích thước căn chỉnh, bằng cách tính toán, (longbits - 0x01010101) & 0x80808080 != 0nó sẽ kiểm tra xem có bất kỳ byte nào trong một số nguyên không. Tính toán này có giá trị dương khi ít nhất một byte cao hơn 0x80, nhưng thường xuyên hơn không nên hoạt động. Nếu đó không phải là trường hợp (vì nó nằm trong khu vực màu vàng), chiều dài được tăng lên bởi kích thước căn chỉnh.

Nếu bất kỳ byte nào trong một số nguyên hóa ra bằng 0 (hoặc 0x81), thì chuỗi được kiểm tra từng byte để xác định vị trí của 0.

Điều này có thể tạo ra một truy cập ngoài giới hạn, tuy nhiên vì nó nằm trong một căn chỉnh, nên nhiều khả năng là không ổn, các đơn vị ánh xạ bộ nhớ thường không có độ chính xác ở mức byte.


Việc thực hiện này là một phần của glibc. Hệ thống GNU bảo vệ bộ nhớ với độ chi tiết của trang. Vì vậy, có, một tải phù hợp bao gồm bất kỳ byte hợp lệ là an toàn.
Peter Cordes

size_tkhông được đảm bảo để được liên kết.
SS Anne

32

Bạn muốn mã phải chính xác, có thể duy trì và nhanh chóng. Những yếu tố này có tầm quan trọng khác nhau:

"Chính xác" là hoàn toàn cần thiết.

"Có thể duy trì" tùy thuộc vào mức độ bạn sẽ duy trì mã: strlen là chức năng thư viện C tiêu chuẩn trong hơn 40 năm. Nó sẽ không thay đổi. Do đó khả năng bảo trì khá không quan trọng - đối với chức năng này.

"Nhanh": Trong nhiều ứng dụng, strcpy, strlen, v.v ... sử dụng một lượng đáng kể thời gian thực hiện. Để đạt được mức tăng tốc độ chung tương tự như việc triển khai strlen phức tạp nhưng không phức tạp này bằng cách cải thiện trình biên dịch sẽ cần những nỗ lực anh hùng.

Nhanh là có một lợi thế khác: Khi các lập trình viên phát hiện ra rằng gọi "strlen" là phương pháp nhanh nhất họ có thể đo số byte trong một chuỗi, họ không còn bị cám dỗ viết mã của riêng mình để làm cho mọi thứ nhanh hơn.

Vì vậy, đối với strlen, tốc độ là quan trọng hơn nhiều và khả năng bảo trì ít quan trọng hơn nhiều so với hầu hết các mã mà bạn sẽ viết.

Tại sao nó phải phức tạp như vậy? Giả sử bạn có chuỗi 1.000 byte. Việc thực hiện đơn giản sẽ kiểm tra 1.000 byte. Việc triển khai hiện tại có thể sẽ kiểm tra các từ 64 bit tại một thời điểm, có nghĩa là 125 từ 64 bit hoặc tám byte. Nó thậm chí có thể sử dụng các hướng dẫn vectơ kiểm tra nói 32 byte mỗi lần, điều này thậm chí còn phức tạp hơn và thậm chí nhanh hơn. Sử dụng các hướng dẫn vectơ dẫn đến mã phức tạp hơn một chút nhưng khá đơn giản, kiểm tra xem một trong tám byte trong từ 64 bit có bằng 0 hay không đòi hỏi một số thủ thuật thông minh. Vì vậy, đối với các chuỗi trung bình đến dài, mã này có thể được dự kiến ​​sẽ nhanh hơn khoảng bốn lần. Đối với một hàm quan trọng như strlen, điều đó đáng để viết một hàm phức tạp hơn.

Tái bút Mã này không phải là rất di động. Nhưng đó là một phần của thư viện Standard C, một phần của việc triển khai - nó không cần phải di động.

PPS. Ai đó đã đăng một ví dụ trong đó một công cụ sửa lỗi đã phàn nàn về việc truy cập các byte qua cuối chuỗi. Việc triển khai có thể được thiết kế để đảm bảo các điều sau: Nếu p là con trỏ hợp lệ cho một byte, thì mọi quyền truy cập vào một byte trong cùng một khối được căn chỉnh sẽ là hành vi không xác định theo tiêu chuẩn C, sẽ trả về giá trị không xác định.

PPPS. Intel đã thêm các hướng dẫn cho các bộ xử lý sau này của họ tạo thành một khối xây dựng cho hàm strstr () (tìm chuỗi con trong chuỗi). Mô tả của họ là tâm trí bogg, nhưng họ có thể làm cho chức năng cụ thể đó có thể nhanh hơn 100 lần. (Về cơ bản, được cung cấp một mảng chứa "Hello, world!" Và một mảng b bắt đầu bằng 16 byte "HelloHelloHelloH" và chứa nhiều byte hơn, nó chỉ ra rằng chuỗi a không xảy ra trong b sớm hơn bắt đầu từ chỉ số 15) .


Hoặc ... Nếu tôi thấy rằng tôi đang thực hiện nhiều quá trình xử lý dựa trên chuỗi và có một nút cổ chai, có lẽ tôi sẽ triển khai phiên bản Chuỗi Pascal của riêng mình thay vì cải thiện strlen ...
Baldrickk

1
Không ai yêu cầu bạn cải thiện strlen. Nhưng làm cho nó đủ tốt để tránh những điều vô nghĩa như mọi người thực hiện chuỗi của riêng họ.
gnasher729


24

Tóm lại: việc kiểm tra một chuỗi byte theo byte sẽ có khả năng chậm trên các kiến ​​trúc có thể lấy một lượng dữ liệu lớn hơn tại một thời điểm.

Nếu việc kiểm tra chấm dứt null có thể được thực hiện trên cơ sở 32 hoặc 64 bit, nó sẽ giảm số lượng kiểm tra mà trình biên dịch phải thực hiện. Đó là những gì mã được liên kết cố gắng thực hiện, với một hệ thống cụ thể. Họ đưa ra các giả định về địa chỉ, căn chỉnh, sử dụng bộ đệm, thiết lập trình biên dịch không chuẩn, v.v.

Đọc từng byte như trong ví dụ của bạn sẽ là một cách tiếp cận hợp lý trên CPU 8 bit hoặc khi viết một lib di động được viết theo tiêu chuẩn C.

Nhìn vào libs tiêu chuẩn C để tư vấn cách viết mã nhanh / tốt không phải là một ý tưởng hay, bởi vì nó sẽ không mang tính di động và dựa trên các giả định không chuẩn hoặc hành vi được xác định kém. Nếu bạn là người mới bắt đầu, đọc mã như vậy có thể sẽ có hại hơn là giáo dục.


1
Tất nhiên, trình tối ưu hóa rất có khả năng hủy đăng ký hoặc tự động vector hóa vòng lặp này và trình nạp trước có thể phát hiện một cách tầm thường mẫu truy cập này. Liệu những thủ thuật này có thực sự quan trọng đối với các bộ xử lý hiện đại hay không cần phải được kiểm tra. Nếu có một chiến thắng để có được nó có lẽ là sử dụng các hướng dẫn vector.
Nga

6
@russbishop: Bạn sẽ hy vọng như vậy, nhưng không. GCC và clang hoàn toàn không có khả năng tự động véc tơ hóa trong đó số lần lặp không được biết trước lần lặp đầu tiên. Điều đó bao gồm các vòng tìm kiếm hoặc bất kỳ vòng lặp nào khác phụ thuộc vào dữ liệu if()break. ICC có thể tự động vector hóa các vòng lặp như vậy, nhưng IDK làm việc tốt như thế nào với một strlen ngây thơ. Và vâng, SSE2 pcmpeqb/ pmovmskbrất tốt cho strlen, thử nghiệm 16 byte tại một thời điểm. code.woboq.org/userspace/glibc/sysdeps/x86_64/strlen.S.html là phiên bản SSE2 của glibc. Xem thêm Q & A này .
Peter Cordes

Oof, đó là điều không may. Tôi thường rất chống UB nhưng khi bạn chỉ ra các chuỗi C yêu cầu đọc kết thúc bộ đệm UB về mặt kỹ thuật để thậm chí cho phép vector hóa. Tôi nghĩ điều tương tự cũng áp dụng cho ARM64 vì nó yêu cầu căn chỉnh.
Nga

-6

Một điều quan trọng không được đề cập bởi các câu trả lời khác là FSF rất thận trọng trong việc đảm bảo rằng mã độc quyền không biến nó thành các dự án GNU. Trong Tiêu chuẩn mã hóa GNU theo các chương trình độc quyền , có một cảnh báo về việc tổ chức thực hiện của bạn theo cách không thể nhầm lẫn với mã độc quyền hiện có:

Trong mọi trường hợp, đừng tham khảo mã nguồn Unix cho hoặc trong khi bạn làm việc trên GNU! (Hoặc cho bất kỳ chương trình độc quyền nào khác.)

Nếu bạn có một hồi ức mơ hồ về các phần bên trong của chương trình Unix, điều này không hoàn toàn có nghĩa là bạn không thể viết một bản nhái của nó, nhưng hãy cố gắng tổ chức bắt chước bên trong dọc theo các dòng khác nhau, bởi vì điều này có thể tạo ra các chi tiết về phiên bản Unix không liên quan và không giống với kết quả của bạn.

Ví dụ, các tiện ích Unix thường được tối ưu hóa để giảm thiểu việc sử dụng bộ nhớ; thay vào đó , nếu bạn đi tốc độ , chương trình của bạn sẽ rất khác.

(Nhấn mạnh của tôi.)


5
Làm thế nào để trả lời câu hỏi này?
SS Anne

1
Câu hỏi trong OP là "mã đơn giản hơn này có hoạt động tốt hơn không?" Và đó là câu hỏi không phải lúc nào cũng được quyết định dựa trên giá trị kỹ thuật. Đối với một dự án như GNU, tránh các cạm bẫy pháp lý là một phần quan trọng của mã "hoạt động tốt hơn" và các triển khai "rõ ràng" strlen()có khả năng xuất hiện tương tự hoặc giống hệt với mã hiện có. Một cái gì đó "điên rồ" khi thực hiện glibc không thể được truy trở lại như thế. Xem xét có bao nhiêu tranh cãi pháp lý đã có trên rangeCheck- 11 dòng mã! - trong cuộc chiến Google / Oracle, tôi muốn nói rằng mối quan tâm của FSF đã được đặt đúng chỗ.
Jack Kelly
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.