glibc의 strlen이 왜 그렇게 빨리 실행되어야합니까?


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
의견은 긴 토론을위한 것이 아닙니다. 이 대화는 채팅 으로 이동 되었습니다 .
Samuel Liew

18
나중에 참조 할 수 있도록 GNU libc의 공식 소스 저장소는 < sourceware.org/git/?p=glibc.git >에 있습니다. < sourceware.org/git/?p=glibc.git;a=blob;f=string/… > 실제로 위와 유사한 코드를 표시합니다. 그러나 sysdeps디렉토리 에서 직접 작성한 어셈블리 언어 구현 은 glibc가 지원하는 대부분의 아키텍처 (대체가없는 가장 일반적으로 사용되는 아키텍처)에서 대신 사용됩니다.
zwol

9
이를 기본적으로 의견 기반으로 폐쇄하기로 투표; "xxx가 xxx에 정말로 필요합니까?" 사람들의 의견에 주관합니다.
SS Anne

2
@ JL2210 : 좋은 지적, 성능이 필요한지 궁금해하지 않는 제목으로 질문의 정신을 포착하도록 제목을 수정했습니다 . 성능을 얻기 위해 왜 이러한 최적화 가 필요한지 .
Peter Cordes

9
@ JL2210 FWIW의 원래 제목은 "C [sic!]에서 너무 복잡해서 왜 그렇게 복잡합니까?"였으며, "너무 광범위"하여 닫히고 다시 열렸다가 "주요 의견 기반"으로 닫혔습니다. 나는 이것을 고치려고 노력했다 ( "당신은 나의 질문을 깨뜨렸다!"와 "당신은 편집력을 남용하고있다!")하면서 문제를 IMVHO는 질문의 기본 전제에 놓여 있었다. 문제가되었습니다 ( "이 코드는 이해하기에는 너무 복잡합니다")는 Q & A에 적합하지 않습니다.-IMO는 답변이 아니라 과외 요청입니다. 나는 60 피트 극으로 다시 만지지 않습니다 :)

답변:


233

당신은 하지 않습니다 필요 당신이 해야 결코 그런 쓰기 코드 - 당신이 C 컴파일러 / 표준 라이브러리 공급 업체 아니에요 특히. strlen매우 의심스러운 속도 해킹과 가정 (어설 션으로 테스트되거나 주석에서 언급되지 않은) 으로 구현 하는 데 사용되는 코드입니다 .

  • unsigned long 4 또는 8 바이트
  • 바이트는 8 비트입니다
  • 포인터로 캐스팅 할 수 있습니다 unsigned long long하지uintptr_t
  • 2 또는 3 개의 최하위 비트가 0인지 확인하여 간단히 포인터를 정렬 할 수 있습니다.
  • 하나는 unsigned longs 로 문자열에 액세스 할 수 있습니다
  • 어떤 영향도없이 배열의 끝을지나 읽을 수 있습니다.

또한 좋은 컴파일러는 다음과 같이 작성된 코드를 대체 할 수도 있습니다.

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함수는 s가 가리키는 문자열의 길이를 계산합니다.

보고

  1. strlen함수는 종료 널 문자 앞에 오는 문자 수를 리턴합니다.

이제 by s가 가리키는 문자열 이 문자열과 종료 NUL을 포함하기에 충분한 길이의 문자 배열에 있다면 null 종료자를지나 문자열에 액세스하면 동작정의되지 않습니다 .

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

따라서 완전히 이식 가능하고 표준을 준수하는 C에서이를 올바르게 구현 하는 유일한 방법은 사소한 변환을 제외하고는 질문에 작성되는 방법입니다 . 루프 등을 풀면 더 빠른 척 할 수는 있지만 여전히 수행해야합니다. 한 번에 한 바이트 .

(해설자들이 지적한 것처럼, 엄격한 이식성이 너무 많은 부담이 될 때, 합리적이거나 알려진 안전한 가정을 이용하는 것이 항상 나쁜 것은 아닙니다. 특히 하나의 특정 C 구현 의 일부인 코드에서는 이해해야합니다. 구부릴 수있는 방법 /시기를 알기 전에 규칙)


링크 된 strlen구현은 먼저 포인터가의 자연 4 또는 8 바이트 정렬 경계를 가리킬 때까지 개별적으로 바이트를 확인합니다 unsigned long. C 표준에 따르면 올바르게 정렬 되지 않은 포인터에 액세스하면 동작정의되지 않으므로 다음 번 트릭이 더 복잡해 지려면 반드시 수행해야합니다. (86 이외의 CPU 아키텍처에서 실제로, 잘못 정렬 된 단어 나 더블 부하가 C입니다. 잘못 것 없는 휴대용 어셈블리 언어하지만,이 코드는 그런 식으로 사용하고 있습니다). 또한 메모리 보호 기능이 정렬 된 블록 (예 : 4kiB 가상 메모리 페이지)에서 작동하는 구현에서 오류가 발생할 위험없이 객체 끝을 지나서 읽을 수있게합니다.

코드 : 이제 더러운 부분은 제공 중단 약속을 4 또는 8 8 비트 시간 (A에서 바이트를 읽고 long int이 있다면 신속하게 알아낼), 및 서명되지 않은 추가로 약간의 트릭을 사용하는 모든 이들 4 또는 8 내에서 0 바이트가 bytes-캐리 비트가 비트 마스크에 의해 잡힌 비트를 변경하도록 특수하게 조작 된 숫자를 사용합니다. 본질적으로 이것은 마스크의 4 또는 8 바이트 중 하나가 이러한 각 바이트를 반복하는 보다 0으로 빠른지 알아 냅니다. 마지막으로 어떤 바이트가 첫 번째 0인지 알아 내고 결과를 반환 하는 루프 가 있습니다.

가장 큰 문제는 경우 sizeof (unsigned long) - 1sizeof (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
다시 : "매우 의심스러운 속도 해킹과 가정"-즉 휴대용 코드에서 매우 의심 스럽다 . 표준 라이브러리는 특정 컴파일러 / 하드웨어 조합을 위해 작성되었으며 언어 정의가 정의되지 않은 것으로 남겨진 것들의 실제 동작에 대한 지식이 있습니다. 그렇습니다. 대부분의 사람들은 그런 코드를 작성해서는 안되지만 표준 라이브러리를 구현할 때 이식성이 나쁘지는 않습니다.
Pete Becker

4
동의하지 마십시오. 아니면 거의. 조기 최적화는 모든 악의 원천입니다. (이 경우 실제로 동기 부여 될 수 있음). 동일한 매우 긴 문자열에서 많은 strlen () 호출을 수행하면 응용 프로그램이 다르게 작성 될 수 있습니다. 예를 들어 문자열을 만들 때 문자열 길이를 변수에 저장하면 strlen ()을 전혀 호출 할 필요가 없습니다.
ghellquist

65
@ghellquist : 자주 사용되는 라이브러리 호출을 최적화하는 것은 "조기 최적화"가 아닙니다.
jamesqf

7
@Antti Haapala : 왜 strlen이 O (1)이어야한다고 생각하십니까? 그리고 우리가 가진 것은 O (n)이지만 여러 가지 상수 곱셈기를 가진 여러 구현입니다. 당신은 그것이 중요하지 않다고 생각할 수도 있지만, 우리 중 일부에게는 마이크로 초 단위로 작동하는 O (n) 알고리즘의 구현이 몇 초 또는 몇 밀리 초가 걸리는 것보다 훨씬 낫습니다. 직업 과정.
jamesqf

8
@PeteBecker : 표준 라이브러리의 맥락에서 (이 경우는 아니지만) 이식 불가능한 코드를 작성하는 것이 표준 라이브러리의 목적이 구현 관련 사항에 대한 표준 인터페이스를 제공하는 것이므로 표준이 될 수 있습니다.
PlasmaHH

148

이것에 대한 세부 사항 / 배경에 대한 의견에 많은 (약간 또는 전적으로) 잘못된 추측이있었습니다.

당신이보고있는 glibc에의 최적화 된 C 대체 최적화 된 구현입니다. (손으로 작성한 asm 구현이없는 ISA의 경우) . 또는 glibc 소스 트리에 여전히있는 해당 코드의 이전 버전입니다. https://code.woboq.org/userspace/glibc/string/strlen.c.html 은 현재 glibc 자식 트리를 기반으로하는 코드 브라우저입니다. 분명히 MIPS를 포함한 몇 가지 주류 glibc 대상에서 여전히 사용됩니다. (@zwol 감사합니다).

x86 및 ARM과 같은 널리 사용되는 ISA에서 glibc는 손으로 쓴 asm을 사용합니다.

따라서이 코드에 대한 내용을 변경하려는 인센티브는 생각보다 적습니다.

이 비트 핵 코드 ( https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord )는 실제로 서버 / 데스크톱 / 랩탑 / 스마트 폰에서 실행되는 코드 가 아닙니다. 순식간에 바이트 단위 루프 보다 낫지 만이 비트 핵조차도 최신 CPU의 효율적인 asm과 비교할 때 상당히 나쁘다 (특히 AVX2 SIMD가 몇 가지 명령으로 32 바이트를 검사 할 수있는 x86, 클럭 당 32 ~ 64 바이트 허용) 데이터가 2 / 클럭 벡터로드 및 ALU 처리량을 가진 최신 CPU의 L1d 캐시에서 핫한 경우 (예 : 시작 오버 헤드가 지배적이지 않은 중간 크기의 문자열의 경우) 주 루프에서 순환합니다.)

glibc는 동적 연결 트릭을 사용 strlen하여 CPU에 가장 적합한 버전으로 확인하므로 x86에도 SSE2 버전 (16 바이트 벡터, x86-64의 기준선) 및 AVX2 버전 (32 바이트 벡터)이 있습니다.

x86은 벡터와 범용 레지스터간에 효율적인 데이터 전송을 제공하므로 루프 제어가 데이터에 의존하는 암시 적 길이 문자열의 기능을 가속화하기 위해 SIMD를 사용하는 것이 독특합니다 (?). pcmpeqb/ pmovmskb한 번에 16 개의 개별 바이트를 테스트 할 수 있습니다.

glibc는 AdvSIMD를 사용하는 것과 같은 AArch64 버전과 vector-> GP 레지스터가 파이프 라인을 정지시키는 AArch64 CPU 버전을 가지고 있으므로 실제로이 비트 핵을 사용합니다 . 그러나 적중이 발생하면 레지스터 내 바이트를 찾기 위해 카운트 선행 0을 사용하고, 페이지 교차를 확인한 후 AArch64의 효율적인 정렬되지 않은 액세스를 활용합니다.

관련 : 최적화가 활성화 된 상태에서이 코드가 6.5 배 더 느린 이유는 무엇입니까? strlen큰 버퍼와 gcc가 인라인하는 방법을 알기에 좋은 간단한 asm 구현 으로 x86 asm의 빠른 속도와 느린 속도에 대한 자세한 내용 이 있습니다. (일부 gcc 버전 rep scasb은 인라인으로 인라인 이 매우 느리거나 한 번에 4 바이트 비트 비트가 나옵니다. 따라서 GCC의 인라인 스트레치 레시피는 업데이트하거나 비활성화해야합니다.)

Asm에는 C 스타일 "정의되지 않은 동작"이 없습니다 . 원하는대로 메모리의 바이트에 액세스하는 것이 안전하며 유효한 바이트를 포함하는 정렬 된 부하는 오류가 발생하지 않습니다. 메모리 보호는 정렬 된 페이지 단위로 수행됩니다. 정렬 된 액세스는 페이지 경계를 넘을 수없는 것보다 좁습니다. x86 및 x64의 동일한 페이지에서 버퍼의 끝을지나 읽는 것이 안전합니까? 이 C 핵이 컴파일러가이 함수의 독립형 비 인라인 구현을 위해 작성하도록하는 머신 코드에도 동일한 이유가 적용됩니다.

컴파일러가 알 수없는 비 인라인 함수를 호출하기위한 코드를 생성 할 때 함수가 전역 변수와 포인터를 가질 수있는 메모리를 수정한다고 가정해야합니다. 즉, 주소를 이스케이프하지 않은 지역 주민을 제외한 모든 것은 전화를 통해 메모리에서 동기화되어야합니다. 이것은 분명히 asm으로 작성된 함수뿐만 아니라 라이브러리 함수에도 적용됩니다. 링크 타임 최적화를 사용하지 않으면 별도의 번역 단위 (소스 파일)에도 적용됩니다.


이 안전한 이유 의 glibc의 한 부분으로하지 , 그렇지 않으면.

가장 중요한 요소는 strlen다른 어떤 것도 인라인 할 수 없다는 것입니다. 안전하지 않습니다. 여기에는 엄격한 앨리어싱 UB (을 char통해 데이터 읽기 unsigned long*)가 포함됩니다. char*다른 별명 아무것도 허용 하지만 그 반대입니다 하지 사실 .

사전 컴파일 라이브러리 (glibc)를위한 라이브러리 함수입니다. 발신자에게 링크 시간 최적화 기능이 제공되지 않습니다. 즉, 독립형 버전의 안전한 기계 코드로 컴파일해야합니다 strlen. 휴대용 / 안전 할 필요는 없습니다. C.

GNU C 라이브러리는 GCC로만 컴파일하면됩니다. 분명히 GNU 확장을 지원하더라도 clang 또는 ICC로 컴파일하는 것은 지원되지 않습니다 . GCC는 C 소스 파일을 기계어 코드의 객체 파일로 만드는 사전 컴파일러입니다. 인터프리터가 아니므로 컴파일 타임에 인라인하지 않으면 메모리의 바이트는 메모리의 바이트 일뿐입니다. 즉, 엄격한 앨리어싱 UB는 서로 다른 유형의 액세스가 서로 인라인되지 않는 다른 기능에서 발생할 때 위험하지 않습니다.

기억 strlen의 행동이 정의 에 의해 ISO C 표준. 이 함수 이름은 구체적으로 구현의 일부입니다 . GCC와 같은 컴파일러는 사용하지 않는 한 이름을 내장 함수로 취급 -fno-builtin-strlen하므로 strlen("foo")컴파일 타임 상수가 될 수 있습니다 3. 라이브러리의 정의는 gcc가 자체 레시피 또는 무언가를 인라이닝하는 대신 실제로 호출하기로 결정한 경우 에만 사용됩니다.

컴파일시 UB가 컴파일러 에 표시되지 않으면 정상적인 기계 코드를 얻습니다. 기계 코드는 UB가 아닌 경우에 작동해야하며 원하는 경우에도 호출자가 데이터를 뾰족한 메모리에 넣는 데 사용하는 유형을 감지 할 수있는 방법이 없습니다.

Glibc는 링크 타임 최적화로 인라인 할 수없는 독립형 정적 또는 동적 라이브러리로 컴파일됩니다. glibc의 빌드 스크립트는 프로그램에 인라인 할 때 링크 타임 최적화를 위해 머신 코드 + gcc GIMPLE 내부 표현을 포함하는 "지방"정적 라이브러리를 생성하지 않습니다. (즉 , 메인 프로그램 libc.a-flto링크 타임 최적화에 참여하지 않을 것입니다 .) glibc를 그런 식으로 빌드 하면 실제로 이것을 사용하는 타겟에서.c 안전하지 않을 수 있습니다 .

사실 @zwol가 언급 한 것처럼 LTO는 glibc 소스 파일간에 인라인이 가능할 경우 깨질 수있는 이와 같은 "취약한"코드 때문에 glibc 자체를 빌드 할 때 사용할 수 없습니다 . ( strlen예를 들어 printf구현의 일부로 내부 사용이 있습니다 )


이것은 strlen몇 가지 가정을합니다.

  • CHAR_BIT8의 배수입니다 . 모든 GNU 시스템에서 적용됩니다. POSIX 2001도 보장 CHAR_BIT == 8합니다. (와 시스템의 안전이 외모 CHAR_BIT= 16또는 32일부의 DSP 등이, 정렬되지 않은 - 프롤로그 루프는 경우 항상 0 반복을 실행할 sizeof(long) = sizeof(char) = 1때마다 포인터가 항상 정렬되어 있기 때문에 p & sizeof(long)-1항상 0이다.)하지만이 아닌 ASCII 문자 세트가 있다면 문자는 9 곳 또는 12 비트 너비 0x8080...는 잘못된 패턴입니다.
  • (아마) unsigned long는 4 또는 8 바이트입니다. 또는 실제로 unsigned long최대 8 크기 까지 작동 하며이를 사용하여 assert()확인합니다.

이 두 가지는 가능한 UB가 아니며 일부 C 구현에는 이식성이 없습니다. 이 코드는 작동하는 플랫폼에서 C 구현의 일부 이거나 그랬으므로 괜찮습니다.

다음 가정은 잠재적 인 C UB입니다.

  • 유효한 바이트를 포함하는 정렬 된로드는 결함이 없으며 실제로 원하는 객체 외부의 바이트를 무시하는 한 안전합니다. (정렬 된 페이지 단위로 메모리 보호가 이루어지기 때문에 모든 GNU 시스템과 모든 일반 CPU에서 asm으로 적용됩니다. x86 및 x64의 동일한 페이지 내에서 버퍼의 끝을지나 읽는 것이 안전합니까? UB의 경우 C에서 안전 합니까? 컴파일 타임에 보이지 않습니다. 인라인이 없으면 여기에 해당됩니다. 컴파일러는 첫 번째 과거를 읽는 0것이 UB 임을 증명할 수 없습니다 . 예를 들어 C char[]배열 이 될 수 있습니다 {1,2,0,3})

마지막 요점은 여기서 C 객체의 끝을 지나서 읽는 것이 안전하다는 것입니다. 현재 컴파일러로 인라인 할 때조차도 매우 안전합니다. 현재 실행 경로를 암시 할 수 없다는 것을 다루지 않기 때문입니다. 그러나 어쨌든 엄격한 앨리어싱은이 인라인을 허용하면 이미 쇼 토퍼입니다.

그런 다음 포인터 캐스팅을 사용 하는 Linux 커널의 오래된 안전하지 않은 memcpy CPP 매크로 와 같은 문제가 있습니다 unsigned long( gcc, 엄격 앨리어싱 및 공포 이야기 ).

이것은 strlen일반적으로 그런 것들로 도망 갈 수 있었던 시대로 거슬러 올라갑니다 . 예전에는 GCC3 이전의 "인라인하지 않을 때만"경고없이 거의 안전했습니다.


콜 / 레트 경계를 볼 때만 보이는 UB는 우리를 해칠 수 없습니다. (예 : char buf[]unsigned long[]캐스트 배열 대신에 호출 const char*). 기계어 코드가 일단 설정되면 메모리의 바이트를 처리합니다. 인라인이 아닌 함수 호출은 수신자가 모든 / 모든 메모리를 읽는 것으로 가정해야합니다.


엄격한 앨리어싱 UB없이 안전하게 작성

GCC 유형 속성은may_alias 유형을 같은 별칭 - 어떤 치료를 제공합니다 char*. (@KonradBorowsk에서 제안). GCC 헤더는 현재 x86 SIMD 벡터 유형에 사용 __m128i하므로 항상 안전하게 할 수 있습니다 _mm_loadu_si128( (__m128i*)foo ). ( 이것의 의미와 의미가 아닌 것에 대한 자세한 내용은 하드웨어 벡터 포인터와 해당 유형 사이의`reinterpret_cast`가 정의되지 않은 동작입니까? 를 참조하십시오.)

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;

ISO에서 앨리어싱로드를 표현하는 이식 가능한 방법은로memcpy , 최신 컴파일러는 단일로드 명령으로 인라인하는 방법을 알고 있습니다. 예 :

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

또한 한 번에 한 번 액세스 memcpy하면 작동 하므로 정렬되지 않은로드에도 작동 char합니다. 그러나 실제로 현대 컴파일러는 memcpy매우 잘 이해 합니다.

여기에 위험은 GCC가없는 경우이다 알고 있는지에 대한 char_ptr워드로 정렬, 그것은 ASM에 정렬되지 않은 부하를 지원하지 않을 수 있습니다 일부 플랫폼에 인라인하지 않습니다. 예 : MIPS64r6 이전의 MIPS 또는 이전 ARM. memcpy단어를로드하고 다른 메모리에 그대로두기 위한 실제 함수 호출 이 있으면 재앙이 될 것입니다. GCC는 때때로 코드가 포인터를 정렬하는 것을 볼 수 있습니다. 또는 한 번에 char-at-a-time 루프 후에 우롱 경계에 도달하면 사용할 수 있습니다.
p = __builtin_assume_aligned(p, sizeof(unsigned long));

이것은 객체로 읽기 가능한 UB를 피하는 것이 아니라 현재 GCC를 사용하면 실제로 위험하지는 않습니다.


수동으로 최적화 된 C 소스가 필요한 이유 : 현재 컴파일러로는 충분하지 않습니다

광범위하게 사용되는 표준 라이브러리 기능에 대한 모든 성능 저하를 원할 때 수동으로 최적화 된 asm이 훨씬 향상 될 수 있습니다. 특히 같은 것 memcpy뿐만 아니라 strlen. 이 경우 SSE2를 활용하기 위해 x86 내장 함수와 함께 C를 사용하는 것이 훨씬 쉬울 것입니다.

그러나 여기서는 ISA 관련 기능이없는 순진한 vs. 비트 핵 C 버전에 대해 이야기하고 있습니다.

(저는 strlen가능한 빨리 실행 하는 것이 널리 사용되는 것으로 간주 할 수 있다고 생각 합니다. 따라서 더 간단한 소스에서 효율적인 머신 코드를 얻을 수 있는지 여부는 문제가됩니다. 아닙니다.)

현재 GCC와 clang은 반복 횟수가 첫 번째 반복보다 먼저 알려지지 않은 루프를 자동 벡터화 할 수 없습니다 . (예를 들어 , 첫 번째 반복을 실행 하기 전에 루프가 최소한 16 회 반복 실행되는지 확인할 수 있어야합니다 .) 컴파일러.

여기에는 검색 루프 또는 if()break카운터뿐만 아니라 데이터 종속적 인 다른 루프가 포함됩니다.

ICC (x86 용 인텔의 컴파일러)는 일부 검색 루프를 자동 벡터화 할 수 있지만 strlenOpenBSD의 libc 사용과 같은 단순 / 순진 C에 대해서는 순시 바이트 당 asm 만 유지 합니다. ( 고드 볼트 ). ( @Peske의 답변에서 ).

손 최적화 된 libc strlen현재 컴파일러의 성능을 위해서는 가 필요합니다 . 메인 메모리가 사이클 당 약 8 바이트를 유지할 수 있고 L1d 캐시가 사이클 당 16-64를 전달할 수있는 경우 한 번에 1 바이트 씩 (와이드 슈퍼 스칼라 CPU에서 사이클 당 2 바이트를 언 롤링하는 경우) 한심한주의를 기울입니다. (Haswell 및 Ryzen 이후 최신 주류 x86 CPU에서주기 당 2x 32 바이트로드. 512 비트 벡터 만 사용하는 경우 클럭 속도를 줄일 수있는 AVX512는 계산하지 않습니다. glibc가 AVX512 버전을 추가하기 위해 서두르지 않을 것입니다. . 256 비트 벡터와, AVX512VL + BW 마스크에 비교 마스크와 만 ktestkortest만들 수 strlen의 마이크로 연산 / 반복을 줄여 더 친화적 인 하이퍼 스레딩.)

여기에 x86이 아닌 것을 포함 시켰습니다. "16 바이트"입니다. 예를 들어 대부분의 AArch64 CPU는 적어도 그렇게 할 수 있습니다. 그리고 일부는 strlen해당로드 대역폭을 유지하기에 충분한 실행 처리량을 갖 습니다.

물론 큰 문자열로 작동하는 프로그램은 일반적으로 길이를 추적하여 암시 적 길이 C 문자열의 길이를 매우 자주 찾는 것을 피해야합니다. 그러나 짧은 길이에서 중간 길이의 성능은 여전히 ​​손으로 작성한 구현의 이점을 제공하며 일부 프로그램은 중간 길이의 문자열에서 strlen을 사용하게됩니다.


12
몇 가지 참고 사항 : (1) 현재 GCC 이외의 컴파일러로는 glibc 자체를 컴파일 할 수 없습니다. (2) 인라인이 허용되면 컴파일러가 UB를 볼 수있는 정확한 종류의 경우 때문에 링크 시간 최적화가 활성화 된 상태에서 glibc 자체를 컴파일 할 수 없습니다. (3) CHAR_BIT == 8은 POSIX 요구 사항입니다 (-2001 개정판 기준, 여기 참조 ). (4) C 폴백 구현은 strlen지원되는 일부 CPU에 사용되며 가장 일반적인 것은 MIPS라고 생각합니다.
zwol

1
흥미롭게도, 엄격 앨리어싱 UB는 __attribute__((__may_alias__))속성 을 사용하여 수정 될 수 있습니다 (이것은 이식 가능하지 않지만 glibc에는 적합합니다).
Konrad Borowski

1
@SebastianRedl :를 통해 객체를 읽거나 쓸 수는 있지만를 통해 객체 (예 :의 일부 ) char*를 읽거나 쓰는 것은 여전히 ​​UB 입니다. 엄격한 앨리어싱 규칙 및 'char *'포인터char char[]long*
Peter Cordes

1
C 및 C ++ 표준 CHAR_BIT에서는 8 이상 ( C11의 qv Annex E)이어야하므로 7 비트 이상 char은 변호사가 걱정해야하는 것이 아닙니다. "UTF-8 문자열 리터럴의 경우 배열 요소는 type char을 가지며 UTF-8로 인코딩 된 멀티 바이트 문자 시퀀스의 문자로 초기화됩니다. "라는 요구 사항에 동기를 부여했습니다 .
Davislor

2
이 분석은 굉장한 답변을 제공하는 것 외에도 현재 비활성화 된 최적화에 직면하여 코드를 더욱 강력하게 만드는 패치를 제안하기에 좋은 기초 인 것 같습니다.
중복 제거기

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

C에서는 효율성에 대해 자세히 추론 할 수 있습니다.

이 코드와 같이 한 번에 둘 이상의 바이트를 테스트하는 것보다 null을 찾는 개별 문자를 반복하는 것이 비효율적입니다.

추가 복잡성은 테스트중인 문자열이 한 번에 두 바이트 이상 (주석에 설명 된대로 긴 단어 경계를 따라) 테스트를 시작하기 위해 올바른 위치에 정렬되고 가정이 확실해야한다는 점에서 비롯됩니다. 코드가 사용될 때 데이터 유형의 크기에 대해서는 위반되지 않습니다.

에서 가장 (전부는 아니지만) 현대 소프트웨어 개발 효율성의 세부 사항에이주의가 필요하거나 추가 코드 복잡성의 비용 가치가 없습니다.

이와 같이 효율성에주의를 기울이는 것이 바람직한 장소 중 하나는 연결 한 예제와 같은 표준 라이브러리입니다.


더 많은 단어 경계에 대한 읽으려면, 볼 이 질문 하고 이 우수한 위키 피 디아 페이지를


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 코드는 자체 asm 구현이없는 ISA에 대한 대체 구현으로 보입니다. strlen아키텍처 에 따라 다른 구현이 있습니다 . 예를 들어, amd64strlen 의 코드 는 asm입니다. 대체가 아닌 GNU 구현도 마찬가지로 지적 한다는 PeterCordes의 의견 / 답변과 유사합니다 .


5
OpenBSD와 GNU 툴에서 최적화 된 다양한 값을 잘 보여줍니다.
Jason

11
glibc의 휴대용 폴백 구현입니다. 모든 주요 ISA는 glibc에서 손으로 쓴 asm 구현을 가지고 있으며, 도움이 될 때 SIMD를 사용합니다 (예 : x86). 참조 code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/...code.woboq.org/userspace/glibc/sysdeps/aarch64/multiarch/...
피터 코르

4
OpenBSD 버전조차도 원본이 피하는 결함이 있습니다! s - str에 결과를 표시 할 수없는 경우 동작 은 정의되지 않습니다 ptrdiff_t.
안티 하 팔라

1
@AnttiHaapala : GNU C에서 최대 객체 크기는 PTRDIFF_MAX입니다. 그러나 mmap적어도 리눅스의 메모리보다 더 많은 메모리를 사용할 수 있습니다 (예 : x86-64 커널의 32 비트 프로세스에서 실패하기 전에 약 2.7GB가 연속 될 수 있습니다). OpenBSD에 관한 IDK; 커널은 return크기 내에서 segfaulting 또는 중지하지 않고 도달 할 수 없습니다. 그러나 그렇습니다. 이론적 인 C UB를 피하는 방어 적 코딩은 OpenBSD가 원하는 것이 될 것이라고 생각할 것입니다. 비록 strlen하지 인라인 수와 실제 컴파일러는 빼기로 컴파일합니다.
Peter Cordes

2
@PeterCordes 정확하게. 오픈 BSD에서 같은 일이, 예를 들어 i386을 조립 : cvsweb.openbsd.org/cgi-bin/cvsweb/src/lib/libc/arch/i386/string/...
dchest

34

요컨대, 이것은 표준 라이브러리가 컴파일되는 컴파일러를 알면 표준 라이브러리가 수행 할 수있는 성능 최적화입니다. 표준 라이브러리를 작성하지 않고 특정 컴파일러에 의존하지 않는 한 이와 같은 코드를 작성해서는 안됩니다. 특히 32 비트 플랫폼에서 4, 64 비트 플랫폼에서 8 등의 정렬 바이트 수를 동시에 처리합니다. 이는 순진 바이트 반복보다 4 배 또는 8 배 빠를 수 있음을 의미합니다.

이 작동 방식을 설명하려면 다음 이미지를 고려하십시오. 여기서 32 비트 플랫폼을 가정합니다 (4 바이트 정렬).

"Hello, world!"의 문자 "H"가 있다고합시다. 에 대한 인수로 문자열이 제공되었습니다 strlen. CPU는 메모리에 정렬되는 것을 좋아하기 때문에 (이상적으로는 address % sizeof(size_t) == 0) 정렬 전의 바이트는 느린 방법을 사용하여 바이트 단위로 처리됩니다.

그런 다음 각 정렬 크기의 청크에 대해 (longbits - 0x01010101) & 0x80808080 != 0 하여 정수 내의 바이트 중 하나가 0인지 여부를 확인합니다. 이 계산은 바이트 중 하나 이상이보다 높을 때 오 탐지율을 0x80가지지 만 더 자주 작동하지 않아야합니다. 그렇지 않은 경우 (노란색 영역에서와 같이) 정렬 크기만큼 길이가 늘어납니다.

정수 내의 바이트 중 하나가 0으로 판명되면 0x81 ) 되면 문자열을 바이트 단위로 검사하여 0의 위치를 ​​결정합니다.

이것은 범위를 벗어난 액세스를 만들 수 있지만, 정렬 범위 내에 있기 때문에 메모리 매핑 유닛은 일반적으로 바이트 레벨 정밀도를 갖지 않습니다.


이 구현은 glibc의 일부입니다. GNU 시스템은 페이지 단위로 메모리를 보호합니다. 따라서 유효한 바이트를 포함하는 정렬 된로드가 안전합니다.
Peter Cordes

size_t정렬이 보장되지는 않습니다.
SS Anne

32

코드가 정확하고 유지 관리 가능하며 빠르기를 원합니다. 이 요소들은 다른 중요성을 가지고 있습니다 :

"올바른"은 절대적으로 필수적입니다.

"유지 관리 가능"은 코드를 얼마나 많이 유지할지에 달려 있습니다. strlen은 40 년 이상 동안 표준 C 라이브러리 함수였습니다. 변경되지 않습니다. 따라서이 기능에있어 유지 보수성은 매우 중요하지 않습니다.

"빠른": 많은 응용 프로그램에서 strcpy, strlen 등은 상당한 양의 실행 시간을 사용합니다. 컴파일러를 개선하여이 복잡하지만 매우 복잡한 strlen 구현과 동일한 전체 속도 향상을 달성하려면 영웅적인 노력이 필요합니다.

프로그래머가 "strlen"을 호출하는 것이 문자열의 바이트 수를 측정 할 수있는 가장 빠른 방법이라는 것을 알게되면 더 빨리 자신의 코드를 작성하려는 유혹을받지 않습니다.

따라서 strlen의 경우 작성하는 대부분의 코드보다 속도가 훨씬 중요하고 유지 관리 성이 훨씬 중요합니다.

왜 그렇게 복잡해야합니까? 1,000 바이트 문자열이 있다고 가정하십시오. 간단한 구현은 1,000 바이트를 검사합니다. 현재 구현에서는 한 번에 64 비트 단어를 검사 할 수 있으며 이는 125 개의 64 비트 또는 8 바이트 단어를 의미합니다. 심지어 한 번에 32 바이트라고 말하는 벡터 명령을 사용할 수도 있습니다. 이는 훨씬 더 복잡하고 빠릅니다. 벡터 명령어를 사용하면 좀 더 복잡하지만 매우 간단한 코드가 만들어 지므로 64 비트 워드에서 8 바이트 중 하나가 0인지 여부를 확인하려면 영리한 트릭이 필요합니다. 따라서 중간에서 긴 문자열의 경우이 코드는 약 4 배 더 빠를 것으로 예상 할 수 있습니다. strlen만큼 중요한 함수의 경우 더 복잡한 함수를 작성하는 것이 좋습니다.

추신. 코드는 이식성이 떨어집니다. 그러나 표준 C 라이브러리의 일부이며 구현의 일부입니다. 이식성이 없어도됩니다.

PPS. 누군가가 디버깅 도구가 문자열 끝을지나 바이트 액세스에 대해 불평하는 예를 게시했습니다. p가 바이트에 대한 유효한 포인터 인 경우 C 표준에 따라 정의되지 않은 동작 인 동일한 정렬 블록의 바이트에 액세스하면 지정되지 않은 값이 반환됩니다.

PPPS. 인텔은 이후 프로세서에 strstr () 함수를위한 빌딩 블록을 구성하는 명령을 추가했습니다 (문자열에 하위 문자열 찾기). 그들의 설명은 정신이 번쩍하지만 특정 기능을 100 배 더 빠르게 만들 수 있습니다. (기본적으로 배열 a에 "Hello, world!"가 포함되고 배열 b에 16 바이트 "HelloHelloHelloH"로 시작하고 더 많은 바이트가 포함 된 경우, 문자열 a는 인덱스 15에서 시작하는 것보다 b에서 일찍 발생하지 않는 것으로 나타납니다. .


또는 ... 문자열 기반 처리를 많이하고 병목 현상이있는 것을 발견 한 경우 아마도 성능 향상 대신 Pascal Strings 자체 버전을 구현할 것입니다.
Baldrickk

1
아무도 당신 에게 strlen을 개선 하라고 요구하지 않습니다. 그러나 그것을 충분히 만들면 사람들이 자신의 현을 구현하는 것처럼 말도 안됩니다.
gnasher729 '


24

간단히 말해서, 바이트 단위로 문자열을 검사하면 한 번에 많은 양의 데이터를 가져올 수있는 아키텍처에서 느려질 수 있습니다.

널 종료 검사가 32 또는 64 비트 단위로 수행 될 수 있으면 컴파일러가 수행해야하는 검사의 양이 줄어 듭니다. 그것이 특정 시스템을 염두에두고 연결된 코드가 시도하는 것입니다. 주소 지정, 정렬, 캐시 사용, 비표준 컴파일러 설정 등에 대한 가정을합니다.

예제와 같이 바이트 단위로 읽는 것은 8 비트 CPU에서 또는 표준 C로 작성된 휴대용 라이브러리를 작성할 때 현명한 접근 방식입니다.

빠르고 좋은 코드를 작성하는 방법에 대한 조언을 얻기 위해 C 표준 라이브러리를 살펴 보는 것은 좋은 아이디어가 아닙니다. 이식 불가능하고 비표준 가정이나 잘못 정의 된 동작에 의존하기 때문입니다. 초보자라면 그러한 코드를 읽는 것이 교육보다 더 해로울 것입니다.


1
물론 옵티마이 저는이 루프를 풀거나 자동 벡터화 할 가능성이 높으며 프리 페처는이 액세스 패턴을 사소하게 감지 할 수 있습니다. 이러한 트릭이 실제로 최신 프로세서에서 중요한지 여부를 테스트해야합니다. 승리를 거두었다면 아마도 벡터 명령을 사용하고있을 것입니다.
russbishop

6
@russbishop : 당신은 그렇게 희망하지만, 아닙니다. GCC와 clang은 반복 횟수가 첫 번째 반복보다 먼저 알려지지 않은 자동 벡터화 루프를 완전히 사용할 수 없습니다. 여기에는 검색 루프 또는 데이터 종속적 인 다른 루프가 포함됩니다 if()break. ICC는 이러한 루프를 자동 벡터화 할 수 있지만 IDK는 순진한 strlen을 얼마나 잘 수행 할 수 있습니다. 그렇습니다. SSE2 pcmpeqb/ pmovmskb는 한 번에 16 바이트를 테스트하여 strlen에 매우 좋습니다. code.woboq.org/userspace/glibc/sysdeps/x86_64/strlen.S.html 은 glibc의 SSE2 버전입니다. 이 Q & A 도 참조하십시오 .
Peter Cordes

죄송합니다. 나는 보통 매우 안티 UB이지만 C 문자열은 벡터화를 허용하기 위해 기술적으로 UB 버퍼 끝 읽기가 필요합니다. 정렬이 필요하기 때문에 ARM64에도 동일하게 적용됩니다.
russbishop

-6

다른 답변에서 언급하지 않은 한 가지 중요한 점은 FSF가 독점 코드로 인해 GNU 프로젝트에 포함되지 않도록하는 데 매우 신중하다는 것입니다. 독점 프로그램 참조 아래 의 GNU 코딩 표준 에는 기존 독점 코드와 혼동 할 수없는 방식으로 구현을 구성하는 것에 대한 경고가 있습니다.

어떤 상황에서도 GNU 작업 중이나 GNU 작업 중에 유닉스 소스 코드를 참조하지 마십시오! (또는 다른 독점 프로그램에 한함)

유닉스 프로그램의 내부를 모호하게 기억한다면, 모방을 작성할 수 없다는 것을 의미하지는 않지만 다른 모방을 따라 내부에서 모방을 조직하려고 시도하십시오. 결과와 관련이없고 유닉스 버전이 다릅니다.

예를 들어, 유닉스 유틸리티는 일반적으로 메모리 사용을 최소화하도록 최적화되었습니다. 대신 속도를 찾으면 프로그램이 매우 다릅니다.

(Emphasis mine.)


5
이 질문에 어떻게 대답합니까?
SS Anne

1
OP의 질문은 "이 간단한 코드가 더 잘 작동하지 않습니까?"라는 것이 었습니다. 이것이 기술적 장점에 대해 항상 결정되는 것은 아닙니다. GNU와 같은 프로젝트의 경우, 법적인 함정을 피하는 것이 "더 잘 작동하는"코드의 중요한 부분이며, "명백한"구현은 strlen()기존 코드와 유사하거나 동일 할 수 있습니다. glibc의 구현처럼 "미친"것 같은 것은 추적 할 수 없습니다. rangeCheck11 줄의 코드 가 합법적이라고 생각합니다 ! — Google / Oracle의 싸움에서 FSF의 우려가 적절하다고 말했습니다.
Jack Kelly
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.