[解決済み】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)
なぜこのバージョンは動作が速いのですか?
無駄な作業を多くしているのでは?
どうすればいい?
あなたは
しない
必要であり、あなたは
絶対にしてはいけない
特に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
としています。
説明
- は
strlen
関数は、s が指す文字列の長さを計算します。戻り値
- は
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
すなわち、悪いことが起こった。
関連
-
[解決済み】ENOENTが「そのようなファイルやディレクトリはありません」という意味であるのはなぜですか?
-
[解決済み】スレッド1:EXC_BAD_ACCESS(コード=1、アドレス=0x0)標準Cメモリ問題
-
[解決済み】「構造体でもユニオンでもないものにメンバー'*******'を要求する」とはどういう意味ですか?
-
[解決済み】「複数の定義」「最初に定義されたのはここです」エラーについて
-
[解決済み】なぜか。"エラー: 配列型を持つ式への代入"
-
[解決済み】makefile:4。*** missing separator. 停止する
-
[解決済み] Cプログラムで「配列の添え字が整数でない」。
-
[解決済み] Cプリプロセッサはなぜ "linux "という単語を定数 "1 "と解釈するのですか?
-
[解決済み] Collatz予想の検証を行うC++のコードは、なぜ手書きのアセンブリよりも高速に動作するのでしょうか?
-
[解決済み] なぜsizeof(x++)はxをインクリメントしないのですか?
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
[解決済み】Valgrind が "Invalid write of size 8" で文句を言う。
-
[解決済み】C 言語の添え字で配列の要素値を代入すると、配列でもポインタでもベクトルでもない値になる
-
[解決済み】 `S_ISREG()` とは何ですか、そして何をするのですか?
-
[解決済み】式は、単純なポインタ演算を使用して完全なオブジェクト型へのポインタでなければなりません【重複】。
-
[解決済み】ヒープ割り当てで初期化されていない値が作成された
-
[解決済み】エラー。非スカラー型への変換を要求された
-
[解決済み】C言語で入力が整数型かどうかチェックする
-
[解決済み】execvp: バッドアドレスエラー
-
[解決済み】Linuxソケットのwrite()でBad File Descriptorが発生するC
-
[解決済み】c - 警告:関数 'printf'の暗黙の宣言