標準Cライブラリの多くの関数、特に文字列操作用の関数、特にstrcpy()は、次のプロトタイプを共有しています。
char *the_function (char *destination, ...)
これらの関数の戻り値は、実際には提供されたdestination
と同じです。冗長なものの戻り値を無駄にするのはなぜですか?そのような関数が無効であるか、何か有用なものを返す方が理にかなっています。
これがなぜであるかについての私の唯一の推測は、次のように、関数呼び出しを別の式にネストする方が簡単で便利であるということです。
printf("%s\n", strcpy(dst, src));
このイディオムを正当化する他の賢明な理由はありますか?
エヴァンが指摘したように、次のようなことをすることが可能です
_char* s = strcpy(malloc(10), "test");
_
例えばヘルパー変数を使用せずに、malloc()ed
メモリに値を割り当てます。
(この例は最良の例ではありません。メモリ不足の状態でクラッシュしますが、アイデアは明らかです)
私はあなたの推測が正しいと信じています、それは電話を入れ子にするのをより簡単にします。
char *stpcpy(char *dest, const char *src);
文字列のendへのポインタを返し、POSIX.1-の一部です。 2008 。それ以前は、1992年以来GNU libc拡張機能でした。1986年にLatticeCAmigaDOSに最初に登場した場合。
_gcc -O3
_は、場合によってはstrcpy
+ strcat
を最適化してstpcpy
またはstrlen
+インラインコピーを使用します。以下を参照してください。
Cの標準ライブラリは非常に初期に設計されており、_str*
_関数が最適に設計されていないと主張するのは非常に簡単です。 I/O関数は間違いなく非常に早い段階で、Cがプリプロセッサを搭載する前の1972年に設計されました。これは なぜfopen(3)
はモード文字列を使用するのですか? Unix open(2)
のようなフラグビットマップの代わりに。
MikeLeskの「ポータブルI/Oパッケージ」に含まれている関数のリストを見つけることができなかったので、現在の形式のstrcpy
がそこまでさかのぼるかどうか、またはそれらの関数が後で追加されたかどうかはわかりません。 。 (私が見つけた唯一の本当の情報源は デニスリッチーの広く知られているCの歴史の記事 です。これは優れていますが、そのの深さではありません。 t実際のI/Oパッケージ自体のドキュメントまたはソースコードを見つけます。)
それらは、1978年に K&R初版 、現在の形で表示されます。
関数は、呼び出し元にとって有用である可能性がある場合は、計算結果を破棄するのではなく、返す必要があります。文字列の終わりへのポインタ、または整数の長さのいずれかとして。 (ポインターは自然です。)
@Rが言うように:
これらの関数が終了ヌルバイトへのポインタを返すことを願っています(これにより、多くの
O(n)
操作がO(1)
に削減されます)
例えばループ内でstrcat(bigstr, newstr[i])
を呼び出して、多くの短い(O(1)長)文字列から長い文字列を作成するのは、およそO(n^2)
の複雑さですが、strlen
/memcpy
は各文字を2回しか調べません。 (strlenで1回、memcpyで1回)。
ANSI C標準ライブラリのみを使用すると、すべての文字を1回だけ効率的に調べる方法はありません。一度に1バイトずつループを手動で作成することもできますが、数バイトより長い文字列の場合、最新のHWで現在のコンパイラ(検索ループを自動ベクトル化しない)で各文字を2回調べるよりも悪いです。与えられた効率的なlibc提供のSIMDstrlenとmemcpy。 length = sprintf(bigstr, "%s", newstr[i]); bigstr+=length;
を使用することもできますが、sprintf()
はそのフォーマット文字列を解析する必要があり、高速ではありません。
差分の位置を返すstrcmp
またはmemcmp
のバージョンすらありません。それが必要な場合は、 Pythonで文字列比較が非常に高速なのはなぜですか? :コンパイルされたループで実行できるものよりも高速に実行される最適化されたライブラリ関数(手がない場合) -関心のあるすべてのターゲットプラットフォーム用に最適化されたasm)。これを使用して、近づくと通常のループにフォールバックする前に、異なるバイトに近づくことができます。
Cの文字列ライブラリは、暗黙の長さの文字列の終わりを見つけるだけでなく、操作のO(n)コスト、およびstrcpy
の動作を考慮せずに設計されたようです。間違いなく唯一の例ではありません。
基本的に、暗黙の長さの文字列を不透明なオブジェクト全体として扱い、検索または追加後に常に先頭または末尾または内部の位置へのポインタを返します。
PDP-11 の初期のCでは、strcpy
はwhile(*dst++ = *src++) {}
よりも効率的ではなかったと思います(おそらくそのように実装されていました)。
実際、 K&R初版(101ページ) は、strcpy
の実装を示しており、次のように述べています。
これは一見不可解に思えるかもしれませんが、表記上の利便性はかなりのものであり、Cプログラムで頻繁に見られる以外の理由がなければ、イディオムを習得する必要があります。
これは、dst
またはsrc
の最終値が必要な場合に、プログラマーが独自のループを作成することを完全に期待していることを意味します。したがって、手作業で最適化されたasmライブラリ関数用のより便利なAPIを公開するには手遅れになるまで、標準ライブラリAPIを再設計する必要性を認識していなかったのかもしれません。
しかし、dst
の元の値を返すことには意味がありますか?
strcpy(dst, src)
dst
を返すことは、_x=y
_がx
に評価することに類似しています。したがって、strcpyは文字列代入演算子のように機能します。
他の回答が指摘しているように、これによりfoo( strcpy(buf,input) );
のようにネストが可能になります。初期のコンピュータは非常にメモリに制約がありました。 ソースコードをコンパクトに保つことは一般的な方法でした。パンチカードと遅い端末がおそらくこれの要因でした。歴史的なコーディング標準やスタイルガイド、または1行にまとめるには多すぎると考えられていたものがわかりません。
無愛想な古いコンパイラもおそらく要因でした。最新の最適化コンパイラでは、char *tmp = foo();
/bar(tmp);
はbar(foo());
より遅くはありませんが、_gcc -O0
_を使用します。非常に初期のコンパイラが変数を完全に最適化できるかどうかはわかりませんが(スタックスペースを予約しないで)、単純なケースでは少なくとも変数をレジスタに保持できることを願っています(意図的にスピル/リロードする最新の_gcc -O0
_とは異なります)。一貫したデバッグのためのすべて)。つまり、_gcc -O0
_は、一貫性のあるデバッグを目的とした反最適化であるため、古いコンパイラには適していません。
C文字列ライブラリの一般的なAPI設計の効率性に注意が払われていないことを考えると、これはありそうにないかもしれません。しかし、おそらくコードサイズの利点がありました。 (初期のコンピューターでは、コードサイズはCPU時間よりも厳しい制限でした)。
初期のCコンパイラーの品質についてはよくわかりませんが、PDP-11のような単純な直交アーキテクチャーであっても、最適化がうまくいかなかったのは間違いありません。
関数呼び出しの後に文字列ポインタが必要になるのは一般的です。 asmレベルでは、おそらくあなた(コンパイラー)は呼び出しの前にそれをレジスターに持っています。呼び出し規約に応じて、スタックにプッシュするか、呼び出し規約が最初の引数を指定する右側のレジスタにコピーします。 (つまり、strcpy
がそれを期待している場所)。または、事前に計画している場合は、呼び出し規約の正しいレジスタにポインタがすでにあります。
しかし、関数呼び出しは、すべての引数受け渡しレジスタを含むいくつかのレジスタをクローバーします。 (したがって、関数がレジスターで引数を取得すると、スクラッチレジスターにコピーする代わりに、そこでインクリメントすることができます。)
したがって、呼び出し元として、関数呼び出し全体で何かを保持するためのcode-genオプションには次のものが含まれます。
dst = strcpy(dst, src);
それ)。すべてのアーキテクチャでのすべての呼び出し規約レジスタ内の戻りポインタサイズの戻り値を知っているので、ライブラリ関数に1つの追加命令があると、その戻り値を使用するすべての呼び出し元のコードサイズを節約できます。
strcpy
(すでにレジスターにある)の戻り値を使用することで、コンパイラーに呼び出しの周りのポインターを呼び出し保存レジスターに保存させたり、スタックにスピルさせたりするよりも、プリミティブな初期Cコンパイラーからより良いasmを取得した可能性があります。これはまだ当てはまるかもしれません。
ところで、多くのISAでは、戻り値レジスタは最初の引数受け渡しレジスタではありません。また、ベース+インデックスアドレッシングモードを使用しない限り、strcpyがポインタインクリメントループのレジスタをコピーするために追加の命令が必要になります(そして別のレジスタを拘束します)。
PDP-11ツールチェーン 通常はある種のスタック引数呼び出し規約を使用 、常に引数をスタックにプッシュします。正常な呼び出し保存レジスタと呼び出しクローバーレジスタの数はわかりませんが、使用できるGPレジスタは5つまたは6つだけでした( R7はプログラムカウンタ、R6はスタックポインタ、R5はよく使用されるフレームポインタ )。つまり、32ビットx86に似ていますが、さらに窮屈です。
_char *bar(char *dst, const char *str1, const char *str2)
{
//return strcat(strcat(strcpy(dst, str1), "separator"), str2);
// more readable to modern eyes:
dst = strcpy(dst, str1);
dst = strcat(dst, "separator");
// dst = strcat(dst, str2);
return dst; // simulates further use of dst
}
# x86 32-bit gcc output, optimized for size (not speed)
# gcc8.1 -Os -fverbose-asm -m32
# input args are on the stack, above the return address
Push ebp #
mov ebp, esp #, Create a stack frame.
sub esp, 16 #, This looks like a missed optimization, wasted insn
Push DWORD PTR [ebp+12] # str1
Push DWORD PTR [ebp+8] # dst
call strcpy #
add esp, 16 #,
mov DWORD PTR [ebp+12], OFFSET FLAT:.LC0 # store new args over our incoming args
mov DWORD PTR [ebp+8], eax # EAX = dst.
leave
jmp strcat # optimized tailcall of the last strcat
_
これは、_dst =
_を使用しないバージョンよりも大幅にコンパクトであり、代わりにstrcat
の入力引数を再利用します。 (Godboltコンパイラエクスプローラの両方を参照してください 。)
_-O3
_の出力は大きく異なります。戻り値を使用しないバージョンのgccは、stpcpy
(テールへのポインターを返します)を使用し、次にmov
- immediateを使用してリテラル文字列データを適切な場所に直接格納します。
ただし、残念ながら、dst = strcpy(dst, src)
-O3バージョンは引き続き通常のstrcpy
を使用し、strcat
をstrlen
+ mov
- immediateとしてインライン化します。
Cの暗黙の長さの文字列は、常に本質的に悪いわけではなく、興味深い利点があります(たとえば、接尾辞もコピーせずに有効な文字列です)。
ただし、C文字列ライブラリは効率的なコードを可能にするように設計されていません。これは、char
- at-a-timeループは通常自動ベクトル化せず、ライブラリ関数は実行する必要のある作業の結果を破棄するためです。
gccとclangは、最初の反復の前に反復カウントがわかっていない限り、ループを自動ベクトル化することはありません。 for(int i=0; i<n ;i++)
。 ICCは検索ループをベクトル化できますが、手書きのasmほどうまくいく可能性はまだありません。
strncpy
などは基本的に災害です。例えばstrncpy
は、バッファサイズの制限に達した場合、終了する_'\0'
_をコピーしません。大きな文字列の途中に書き込むように設計されているようですが、バッファオーバーフローを回避するためにnotではありません。最後にポインタを返さないということは、前後に_arr[n] = 0;
_する必要があることを意味し、触れる必要のないメモリのページに触れる可能性があります。
snprintf
のようないくつかの関数は使用可能であり、常にヌル終了します。どちらを行うかを覚えるのは難しく、間違ったことを覚えていると大きなリスクがあります。そのため、正確性が重要な場合は毎回チェックする必要があります。
ブルース・ドーソンが言うように: すでにstrncpyの使用をやめなさい! 。どうやら__snprintf
_のようないくつかのMSVC拡張機能はさらに悪いです。
また、コーディングも非常に簡単です。
戻り値は通常、AXレジスタに残されます(必須ではありませんが、多くの場合そうです)。そして、関数の開始時に宛先がAXレジスタに格納されます。宛先を返すために、プログラマーは何もする必要がありません....まったく何もしません!値をそのままにしておきます。
プログラマーは関数をvoid
として宣言できます。しかし、その戻り値はすでに適切な場所にあり、返されるのを待っているだけであり、それを返すための追加の指示も必要ありません!どんなに小さな改善でも、場合によっては便利です。
Fluent Interfaces と同じ概念。コードをより速く/読みやすくするだけです。