1. ホーム
  2. c

[解決済み】glibcのstrlenはなぜこんなに複雑でないと高速に実行できないのか?

2022-03-29 06:59:05

質問

を見ていたら 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)

なぜこのバージョンは動作が速いのですか?

無駄な作業を多くしているのでは?

どうすればいい?

あなたは しない 必要であり、あなたは 絶対にしてはいけない 特にCコンパイラや標準ライブラリのベンダでない場合は、そのようなコードを書いてください。これは strlen には、非常に疑わしいスピードハックと仮定(アサーションでテストされておらず、コメントでも言及されていない)が含まれています。

  • unsigned long は4バイトまたは8バイト
  • バイトは8ビット
  • へのポインタをキャストすることができます。 unsigned long long であって uintptr_t
  • ポインタの最下位2~3ビットが0であることを確認するだけで、ポインタを揃えることができます。
  • として文字列にアクセスすることができます。 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ビットバイトを読み出します。 long int があるかどうか、符号なし加算のビットトリックを使って素早く判断します。 任意の キャリービットがビットマスクに引っかかるビットを変更させるような特別な細工をした数値を使用します。 要するにこれは、マスク内の4バイトまたは8バイトのいずれかがゼロであるかどうかを判断するものです。 より速く 各バイトをループで処理するよりも。最後に、次のようなループがあります。 どの バイトが最初の0であれば、その結果を返します。

最大の問題は sizeof (unsigned long) - 1 のタイムアウト。 sizeof (unsigned long) の場合、文字列の終端を越えて読み込まれます - ヌルバイトが 最後の アクセスされたバイト (つまり、リトルエンディアンで最上位、ビッグエンディアンで最下位) を処理します。 ない は、境界外の配列にアクセスします。


を実装するために使用されているにもかかわらず、このコードは strlen をCの標準ライブラリに組み込むと 悪い コードになります。実装で定義された部分と 未定義の部分があり 使ってはいけません どこでも の代わりに、システムが提供する strlen - という関数にリネームしました。 the_strlen をここに追加し、以下のように main :

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

バッファのサイズは慎重に決められ、ちょうど hello world という文字列とターミネータがあります。しかし、私の64ビットプロセッサーでは unsigned long は8バイトなので、後半の部分へのアクセスはこのバッファを越えてしまいます。

でコンパイルすると -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

すなわち、悪いことが起こった。