glibc の strlen を高速に実行するためになぜ複雑にする必要があるのでしょうか? 質問する

glibc の strlen を高速に実行するためになぜ複雑にする必要があるのでしょうか? 質問する

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 ([email protected]),
   with help from Dan Sahlin ([email protected]);
   commentary by Jim Blandy ([email protected]).

   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)

このバージョンはなぜ速く動作するのでしょうか?

無駄な作業をたくさんやっているのではないでしょうか?

ベストアンサー1

このようなコードは必要ないし、絶対に書いてはいけません。特に C コンパイラや標準ライブラリのベンダーでない場合はなおさらです。これは、strlen非常に疑わしいスピード ハックや仮定 (アサーションでテストされていないか、コメントで言及されていない) を実装するために使用されるコードです。

  • unsigned long4バイトまたは8バイト
  • バイトは8ビット
  • ポインタはキャストできるunsigned long longが、キャストできないuintptr_t
  • 最下位2ビットまたは3ビットがゼロであることを確認するだけでポインタを整列させることができる。
  • unsigned long文字列にはsとしてアクセスできる
  • 配列の末尾を超えても悪影響はありません。

さらに、優れたコンパイラは、次のように書かれたコードを置き換えることもできる。

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関数は、終了のヌル文字の前の文字数を返します。

ここで、 が指す文字列が、s文字列と終端のNUL文字をちょうど含むのに十分な長さの文字の配列である場合、ヌル終端文字を超えて文字列にアクセスすると、動作は未定義なります。たとえば、

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

したがって、完全に移植可能で標準に準拠した C でこれを正しく実装する唯一の方法は、些細な変換を除いて、質問に記述されている方法です。ループを展開するなどして高速化を図ることはできますが、それでも一度に1 バイトずつ実行する必要があります。

(コメント投稿者が指摘しているように、厳密な移植性が負担になりすぎる場合、妥当または安全であることがわかっている仮定を利用することは必ずしも悪いことではありません。特に、特定の C 実装の一部であるコードではそうです。ただし、ルールをいつどのように曲げることができるかを知る前に、ルールを理解する必要があります。)


リンクされたstrlen実装は、まず、ポインタが の自然な 4 バイトまたは 8 バイトの境界を指すまで、バイトを個別にチェックしますunsigned long。C 標準では、適切に整列されていないポインタにアクセスすると未定義の動作が発生するとされているため、次のダーティ トリックがさらにダーティになるためには、これを必ず実行する必要があります。(実際には、x86 以外の一部の CPU アーキテクチャでは、整列されていないワードまたはダブルワードのロードはエラーになります。C は移植可能なアセンブリ言語ではありませんが、このコードではそのように使用しています)。また、これにより、メモリ保護が整列ブロックで機能する実装 (例: 4kiB 仮想メモリ ページ) でエラーが発生するリスクなしに、オブジェクトの末尾を超えて読み取ることができます。

ここからが厄介な部分です。コードは約束を破り、一度に 4 または 8 個の 8 ビット バイト (a long int) を読み取り、符号なし加算によるビット トリックを使用して、それらの 4 または 8 バイト内にゼロ バイトがあるかどうかをすばやく判断します特別に細工された数値を使用して、キャリー ビットがビット マスクによってキャッチされたビットを変更するようにします。本質的には、これにより、マスク内の 4 バイトまたは 8 バイトのいずれかがゼロであるかどうかを判断しますが、これは、これらの各バイトをループするよりも高速であると考えられます。最後に、最初のゼロがあるバイト (ある場合) を判断し、結果を返すループが最後にあります。

最大の問題は、sizeof (unsigned long) - 1多くのsizeof (unsigned long)場合、文字列の末尾を超えて読み取られることです。ヌル バイトが最後にアクセスされたバイト (つまり、リトルエンディアンでは最上位、ビッグエンディアンでは最下位) にある場合にのみ、配列の範囲外にアクセスしません。


strlenこのコードは、 C 標準ライブラリで実装するために使用されていますが、不適切なコードです。実装定義と未定義の側面がいくつか含まれており、システム提供の代わりとしてどこでもstrlen使用すべきではありません。関数の名前をthe_strlenhere に変更し、次のコードを追加しました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

つまり、悪いことが起こったのです。

おすすめ記事