ループ状態で使用した場合、strlenは複数回計算されますか?


109

次のコードが冗長な計算を引き起こす可能性があるかどうか、またはコンパイラ固有ですか?

for (int i = 0; i < strlen(ss); ++i)
{
    // blabla
}

ウィルstrlen()たびに計算されi増加しましたか?


14
ループ内で 'ss'が変更されないことを検出できる高度な最適化がなければ、そうだと思います。コンパイルして、アセンブリを見て確認するのが最善です。
MerickOWA

6
これは、コンパイラー、最適化レベル、およびssループ内で(可能性がある)何をするかに依存します。
Hristo Iliev 2012

4
コンパイラがss変更されていないことを証明できる場合、ループから計算を引き上げることができます。
ダニエルフィッシャー

10
@Mike:「strlenが何をするかを正確にコンパイル時に分析する必要がある」-strlenはおそらく組み込み関数であり、その場合、オプティマイザは何をするかを知っています。
スティーブジェソップ

3
@MikeSeymour:多分ないかもしれませんし、多分ありません。strlenはC言語標準で定義されており、その名前は言語で定義された使用のために予約されているため、プログラムは別の定義を自由に提供できません。コンパイラとオプティマイザは、strlenがその入力にのみ依存し、それまたはグローバル状態を変更しないと想定する権利があります。ここでの最適化の課題は、ssが指すメモリがループ内のコードによって変更されないことを確認することです。これは、特定のコードによっては、現在のコンパイラで完全に実現可能です。
Eric Postpischil、2007

回答:


138

はい、strlen()反復ごとに評価されます。理想的な状況下では、オプティマイザは値が変わらないと推測できる可能性がありますが、私は個人的にはそれに依存しません。

私は次のようなことをします

for (int i = 0, n = strlen(ss); i < n; ++i)

または多分

for (int i = 0; ss[i]; ++i)

反復中に文字列の長さが変化しない限り。可能であれば、strlen()毎回呼び出すか、より複雑なロジックで処理する必要があります。


14
文字列を操作していないことがわかっている場合は、基本的にはstrlenとにかく実行されるループであるため、2番目の方法がはるかに望ましいです。
mlibby

26
@alk:文字列が短くなる可能性がある場合、これらの両方が間違っています。
マイクシーモア

3
@alk:文字列を変更する場合、forループはおそらく各文字を反復する最良の方法ではありません。whileループの方が直接的で、インデックスカウンターの管理が簡単だと思います。
mlibby

2
理想的な状況には、LinuxでのGCCでのコンパイルが含まstrlen__attribute__((pure))ます。ここで、は、コンパイラが複数の呼び出しを省略できるようにマークされています。GCCの属性
デビッドロドリゲス-dribeas

6
2番目のバージョンは、理想的で最も慣用的な形式です。これにより、文字列を2回ではなく1回だけ渡すことができます。これにより、長い文字列のパフォーマンス(特にキャッシュの一貫性)が大幅に向上します。
R .. GitHub STOP HELPING ICE

14

はい、ループを使用するたびに。その後、毎回文字列の長さを計算します。次のように使用します。

char str[30];
for ( int i = 0; str[i] != '\0'; i++)
{
//Something;
}

上記のコードstr[i]ではi、ループがサイクルを開始するたびに、位置の文字列内の特定の1文字のみを検証するため、必要なメモリが少なくなり、より効率的です。

詳細については、このリンクを参照してください。

以下のコードでは、ループが実行strlenされるたびに文字列全体の長さがカウントされるため、効率が低下し、より多くの時間とメモリが必要になります。

char str[];
for ( int i = 0; i < strlen(str); i++)
{
//Something;
}

3
「より効率的」に同意できますが、メモリ使用量は少なくなりますか?私が考えることができる唯一のメモリ使用量の違いは、呼び出し中の呼び出しスタックにありstrlenます。そのようにタイトに実行している場合は、おそらく他のいくつかの関数呼び出しも除外することを検討する必要があります...
CVn

@MichaelKjörling "strlen"を使用する場合、ループではループが実行されるたびに文字列全体をスキャンする必要がありますが、上記のコードでは "str [ix]"は、各サイクルで1つの要素のみをスキャンします。場所が「ix」で表されるループ。したがって、「strlen」よりもメモリ使用量が少なくなります。
codeDEXTER 2012

1
それが実際に理にかなっているかどうかはわかりません。strlenの非常にナイーブな実装はint strlen(char *s) { int len = 0; while(s[len] != '\0') len++; return len; }、あなたの答えのコードであなたがやっていることと全く同じです。文字列を2回繰り返すよりも1回繰り返すほうが時間効率が良いとは主張していませんが、どちらかが多かれ少なかれメモリを使用していることはありません。または、文字列の長さを保持するために使用される変数を参照していますか?
CVn

@MichaelKjörling上記の編集されたコードとリンクを参照してください。また、メモリについては、ループが実行されるたびに、反復するすべての値がメモリに格納されます。また、「strlen」の場合、文字列全体を何度もカウントするため、さらに多くのメモリを格納する必要があります。また、Javaとは異なり、C ++には「ガベージコレクター」がないためです。その後、私も間違っている可能性があります。C ++での「ガベージコレクター」の欠如に関するリンクを参照してください。
codeDEXTER 2012

1
@ aashis2sガベージコレクタの欠如は、ヒープ上にオブジェクトを作成するときにのみ役割を果たします。スタック上のオブジェクトは、スコープと終了と同時に破棄されます。
Ikke

9

良いコンパイラーは毎回それを計算するわけではないかもしれませんが、すべてのコンパイラーがそれを行うとは確信できません。

それに加えて、コンパイラstrlen(ss)はそれを変更しないことを知っている必要があります。これはssforループで変更されない場合にのみ当てはまります。

たとえばssforループ内で読み取り専用関数を使用するが、ss-parameterをとして宣言しないconst場合、コンパイラーはそれssがループ内で変更されていないことを認識できずstrlen(ss)、すべての反復で計算する必要があります。


3
+1:ループ内ssで変更してはなりませんfor。ループで呼び出された関数からアクセスおよび変更できないようにする必要があります(引数として渡されるため、またはグローバル変数またはファイルスコープ変数のため)。const-qualificationも要因となる可能性があります。
ジョナサンレフラー

4
コンパイラが「ss」が変更されないことを認識できた可能性は非常に低いと思います。コンパイラーが「ss」を変更する可能性があることを認識していない「ss」内のメモリーを指す迷子ポインターがある可能性があります
MerickOWA

ジョナサンは正しい。ローカルのconst文字列は、コンパイラが「ss」を変更する方法がないことを保証する唯一の方法かもしれない。
MerickOWA

2
@MerickOWA:確かに、それrestrictはC99の目的の1つです。
スティーブジェソップ

4
あなたの最後のパラについて:あなたは上の読み取り専用の関数を呼び出す場合はss、forループでは、そのパラメータが宣言されている場合でもconst char*、コンパイラはまだ()それは知っているいずれかの場合を除き長さを再計算する必要があるssのconstオブジェクトへのポイントを、単にconstへのポインタであるのとは対照的に、または(b)関数をインライン化するか、そうでなければ読み取り専用であることを確認できます。変更されたオブジェクトがconstでなく、文字列リテラルではない場合、キャストして変更することは有効であるため、const char*パラメーターを取得することは、ポイントされたデータを変更しないという約束char*ではありません。
スティーブジェソップ

4

もしssタイプのものでありconst char *、あなたは離れてキャストしていないconstコンパイラのみ呼び出すことができますループ内でネスをstrlen最適化がオンになっている場合は、一度。しかし、これは確かに信頼できる行動ではありません。

strlen結果を変数に保存し、この変数をループで使用する必要があります。追加の変数を作成したくない場合は、何をしているのかに応じて、ループを逆にして逆方向に反復することで回避できます。

for( auto i = strlen(s); i > 0; --i ) {
  // do whatever
  // remember value of s[strlen(s)] is the terminating NULL character
}

1
電話をかけるのは間違いstrlenです。最後まで打つだけループします。
R .. GitHub ICE HELPING ICEの停止

i > 0?それはi >= 0ここにないのですか?個人的にはstrlen(s) - 1、文字列を逆方向に反復する場合にも開始し、終了\0する場合は特別な考慮は必要ありません。
CVn

2
@MichaelKjörling i >= 0は、に初期化した場合にのみ機能しstrlen(s) - 1ますが、文字列の長さがゼロの場合、初期値がアンダーフローします
Praetorian

@プレアン、長さゼロの文字列の良い点。私がコメントを書いたとき、私はそのケースを考慮しませんでした。C ++ i > 0は最初のループエントリで式を評価しますか?そうでない場合、あなたは正しいです、長さゼロのケースは間違いなくループを壊します。もしそうなら、あなたは「単純に」署名されたi== -1 <0を得るので、条件がであればループエントリはありませんi >= 0
CVn

@MichaelKjörlingはい、終了条件は、ループを初めて実行する前に評価されます。strlenの戻り値の型は符号なしであるため、(strlen(s)-1) >= 0長さがゼロの文字列の場合はtrueと評価されます。
Praetorian

3

正式にははい、strlen()すべての反復で呼び出されることが期待されます。

とにかく、いくつかの賢いコンパイラ最適化の存在の可能性を否定したくないので、最初の呼び出しの後のstrlen()への連続した呼び出しを最適化します。


3

全体の述語コードは、forループのすべての反復で実行されます。strlen(ss)呼び出しの結果をメモするために、コンパイラは少なくとも

  1. 機能strlenは副作用なしでした
  2. が指すメモリssは、ループの間変化しません

コンパイラはこれらのどちらも認識していないため、最初の呼び出しの結果を安全にメモできません


まあそれ静的分析でそれらのことを知ることができましたが、そのような分析は現在どのC ++コンパイラにも実装されていないというのがあなたのポイントだと思います、そうですか?
GManNickG

@GManNickGそれは間違いなく#1を証明できますが、#2は難しいです。シングルスレッドの場合は確かにそれを証明できますが、マルチスレッド環境の場合はそうではありません。
JaredPar

1
たぶん私は頑固ですが、2番目はマルチスレッド環境でも可能だと思いますが、非常に強力な推論システムなしでは絶対に不可能ではありません。ただここを黙想するだけです。間違いなく現在のC ++コンパイラの範囲を超えています。
GManNickG

@GManNickG C / C ++でも可能だとは思いません。私は非常に簡単のアドレスを隠しておくことができsssize_tまたはいくつかの中でそれを分割byte値。私の不正なスレッドはそのアドレスにバイトを書き込むだけで、コンパイラはそれがに関連していることを理解する方法を知っているでしょうss
JaredPar

1
@JaredPar:気をつけて申し訳ありませんが、初期化されていないintのことを実行したり、スタックをクロールして意図的に変更したりすることにint a = 0; do_something(); printf("%d",a);基づいて、最適化できないと主張do_something()できますa。実際のところ、gcc 4.5はdo_something(); printf("%d",0);-O3 を使用して最適化します
Steve Jessop

2

はい。strlenは、iが増加するたびに計算されます。

あなたがいる場合、SSを変更しなかったとのループで、それは手段のロジックには影響しませんそれ以外の場合は、影響を与えます。

次のコードを使用する方が安全です。

int length = strlen(ss);

for ( int i = 0; i < length ; ++ i )
{
 // blabla
}

2

はい、strlen(ss)反復ごとに長さが計算されます。あなたは増加している場合はss何らかの方法によっても増加しますi。無限ループになるでしょう。


2

はい、strlen()関数はループが評価されるたびに呼び出されます。

効率を向上させたい場合は、常にローカル変数にすべてを保存することを忘れないでください...時間はかかりますが、非常に便利です。

以下のようなコードを使用できます。

String str="ss";
int l = strlen(str);

for ( int i = 0; i < l ; i++ )
{
    // blablabla
}


2

今日では一般的ではありませんが、20年前の16ビットプラットフォームでは、これをお勧めします。

for ( char* p = str; *p; p++ ) { /* ... */ }

コンパイラの最適化があまり良くない場合でも、上記のコードを使用すると、優れたアセンブリコードが得られます。


1

はい。テストは、ssがループ内で変更されないことを認識していません。あなたがそれが変わらないことを知っているなら、私は書くでしょう:

int stringLength = strlen (ss); 
for ( int i = 0; i < stringLength; ++ i ) 
{
  // blabla 
} 

1

ああ、それは、理想的な状況下でさえ、気を悪くするでしょう!

今日(2018年1月)およびgcc 7.3とclang 5.0の時点で、次のようにコンパイルした場合:

#include <string.h>

void bar(char c);

void foo(const char* __restrict__ ss) 
{
    for (int i = 0; i < strlen(ss); ++i) 
    {
        bar(*ss);
    }
}    

だから、私たちは持っています:

  • ss 定数ポインタです。
  • ss マークされています __restrict__
  • ループ本体は、が指しているメモリに決して接触することはできませんss(まあ、に違反しない限り__restrict__)。

そして、まだ、両方のコンパイラは、実行strlen() 、ループのすべての単一の反復を。すごい。

これは、@ Praetorianと@JaredParの暗示/希望的思考がうまくいかないことも意味します。


0

はい、簡単な言葉で。そして、変更がまったく行われていないことが判明した場合の最適化ステップとして、コンパイラーが望んでいるまれな状態はわずかssです。しかし、安全な状態では、それをYESと考えるべきです。multithreadedイベント駆動型プログラムのような状況があり、それをNOと見なすとバグが発生する可能性があります。プログラムの複雑さをあまり改善しないので、安全にプレイしてください。


0

はい。

strlen()i増加するたびに計算され、最適化されません。

以下のコードは、コンパイラーが最適化してはならない理由を示していますstrlen()

for ( int i = 0; i < strlen(ss); ++i )
{
   // Change ss string.
   ss[i] = 'a'; // Compiler should not optimize strlen().
}

その特定の変更を行ってもssの長さは変更されず、その内容のみが変更されるため、(本当に、非常に賢い)コンパイラーは引き続き最適化できますstrlen
ダレンクック

0

簡単にテストできます:

char nums[] = "0123456789";
size_t end;
int i;
for( i=0, end=strlen(nums); i<strlen(nums); i++ ) {
    putchar( nums[i] );
    num[--end] = 0;
}

ループ条件は、ループを再開する前に、繰り返しごとに評価されます。

また、文字列の長さを処理するために使用するタイプにも注意してください。stdioでsize_t定義されているものでなければなりませんunsigned int。比較してキャストすると、int深刻な脆弱性の問題が発生する可能性があります。


0

まあ、誰かがそれが「賢い」現代のコンパイラによってデフォルトで最適化されていると言っていることに気づきました。ちなみに、最適化せずに結果を見てください。私が試した:
最小限のCコード:

#include <stdio.h>
#include <string.h>

int main()
{
 char *s="aaaa";

 for (int i=0; i<strlen(s);i++)
  printf ("a");
 return 0;
}

私のコンパイラ:g ++(Ubuntu / Linaro 4.6.3-1ubuntu5)4.6.3
アセンブリコードを生成するためのコマンド:g ++ -S -masm = intel test.cpp

Gotten assembly code at the output:
    ...
    L3:
mov DWORD PTR [esp], 97
call    putchar
add DWORD PTR [esp+40], 1
    .L2:
     THIS LOOP IS HERE
    **<b>mov    ebx, DWORD PTR [esp+40]
mov eax, DWORD PTR [esp+44]
mov DWORD PTR [esp+28], -1
mov edx, eax
mov eax, 0
mov ecx, DWORD PTR [esp+28]
mov edi, edx
repnz scasb</b>**
     AS YOU CAN SEE it's done every time
mov eax, ecx
not eax
sub eax, 1
cmp ebx, eax
setb    al
test    al, al
jne .L3
mov eax, 0
     .....

文字列のアドレスがrestrict修飾されていない限り、それを最適化しようとしたコンパイラを信頼するのは嫌です。このような最適化が正当である場合もありますが、そのようなケースがない場合にそのようなケースを確実に特定するために必要な労力restrictは、妥当な方法で、ほぼ確実にメリットを超えます。const restrictただし、文字列のアドレスに修飾子が含まれている場合は、それだけで、他に何も見なくても最適化を正当化できます。
スーパーキャット2017年

0

プレトリアの答えについて詳しく述べると、次のことをお勧めします。

for( auto i = strlen(s)-1; i > 0; --i ) {foo(s[i-1];}
  • autostrlenが返すタイプを気にしたくないからです。C ++ 11コンパイラー(たとえばgcc -std=c++0x、完全にC ++ 11ではないが、自動型は機能します)がそれを行います。
  • i = strlen(s)比較したいので0(下記参照)
  • i > 0 0との比較は、他の数値と比較して(わずかに)速いためです。

欠点はi-1、文字列の文字にアクセスするために使用する必要があることです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.