これは上級管理職から聞かれたインタビューの質問でした。
どちらが速いですか?
while(1) {
// Some code
}
または
while(2) {
//Some code
}
while
内の式は最終的にtrue
またはfalse
に評価されるので、両方とも同じ実行速度を持つと私は言った。この場合、両方ともtrue
と評価され、while
条件内に余分な条件付き命令はありません。そのため、両方の実行速度は同じになり、(1)が優先されます。
しかしインタビュアーは自信を持って言った:「あなたの基本をチェックしなさい。while(1)
はwhile(2)
より速いです」。 (彼は私の自信をテストしていませんでした)
これは本当ですか?
どちらのループも無限ですが、繰り返しごとにどちらがより多くの命令/リソースを使用するかを見ることができます。
Gccを使用して、私は次の2つのプログラムをさまざまなレベルの最適化でAssemblyにコンパイルしました。
int main(void) {
while(1) {}
return 0;
}
int main(void) {
while(2) {}
return 0;
}
最適化なし(-O0
)がなくても、生成されたアセンブリは両方のプログラムで同一でした 。 したがって、2つのループに速度差はありません。
参考として、生成されたアセンブリは次のとおりです(最適化フラグ付きのgcc main.c -S -masm=intel
を使用)。
-O0
の場合:
.file "main.c"
.intel_syntax noprefix
.def __main; .scl 2; .type 32; .endef
.text
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
Push rbp
.seh_pushreg rbp
mov rbp, rsp
.seh_setframe rbp, 0
sub rsp, 32
.seh_stackalloc 32
.seh_endprologue
call __main
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
-O1
の場合:
.file "main.c"
.intel_syntax noprefix
.def __main; .scl 2; .type 32; .endef
.text
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
sub rsp, 40
.seh_stackalloc 40
.seh_endprologue
call __main
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
-O2
と-O3
(同じ出力)の場合:
.file "main.c"
.intel_syntax noprefix
.def __main; .scl 2; .type 32; .endef
.section .text.startup,"x"
.p2align 4,,15
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
sub rsp, 40
.seh_stackalloc 40
.seh_endprologue
call __main
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
実際、ループに対して生成されたアセンブリは、どのレベルの最適化でも同じです。
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
重要なことは:
.L2:
jmp .L2
議会はあまりよく読めませんが、これは明らかに無条件のループです。 jmp
命令は、値をtrueと比較することなく、無条件にプログラムを.L2
ラベルにリセットします。もちろん、プログラムが何らかの理由で終了するまではすぐに再実行されます。これはC/C++コードに直接対応します。
L2:
goto L2;
編集する
興味深いことに、最適化なしを使用しても、次のループはすべてAssemblyでまったく同じ出力(無条件jmp
)を生成しました。
while(42) {}
while(1==1) {}
while(2==2) {}
while(4<7) {}
while(3==3 && 4==4) {}
while(8-9 < 0) {}
while(4.3 * 3e4 >= 2 << 6) {}
while(-0.1 + 02) {}
そして驚いたことに:
#include<math.h>
while(sqrt(7)) {}
while(hypot(3,4)) {}
ユーザー定義関数では、物事がもう少し面白くなります。
int x(void) {
return 1;
}
while(x()) {}
#include<math.h>
double x(void) {
return sqrt(7);
}
while(x()) {}
-O0
では、これら2つの例は実際にx
を呼び出し、各反復ごとに比較を実行します。
最初の例(1を返す):
.L4:
call x
testl %eax, %eax
jne .L4
movl $0, %eax
addq $32, %rsp
popq %rbp
ret
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
2番目の例(sqrt(7)
を返す):
.L4:
call x
xorpd %xmm1, %xmm1
ucomisd %xmm1, %xmm0
jp .L4
xorpd %xmm1, %xmm1
ucomisd %xmm1, %xmm0
jne .L4
movl $0, %eax
addq $32, %rsp
popq %rbp
ret
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
ただし、-O1
以上では、どちらも前の例と同じアセンブリを生成します(前のラベルに戻る無条件のjmp
)。
GCCでは、異なるループは同一のアセンブリにコンパイルされます。コンパイラーは定数値を評価し、実際の比較を実行するのを面倒にしません。
物語の教訓は次のとおりです。
はい、while(1)
はwhile(2)
よりはるかに高速です、人間が読むことができます! _ while(1)
を見慣れないコードベースで見れば、私はすぐに作者の意図を知り、次の行に進むことができます。
while(2)
が表示された場合は、おそらくトラックを中止して、なぜ著者がwhile(1)
を書かなかったのかを考えてみます。作者の指がキーボードを滑りましたか。このコードベースのメンテナは、ループを違った外観にするための曖昧なコメントメカニズムとしてwhile(n)
を使っていますか?壊れた静的解析ツールにおける誤った警告に対する粗雑な回避策でしょうか。それとも、私が生成コードを読んでいるという手がかりでしょうか。それは悪いアドバイス付きのfind-and-replace-allによるものなのか、それとも悪いマージなのか、それとも宇宙線によるものなのでしょうか。たぶん、このコード行は劇的に異なる何かをするはずです。たぶんそれはwhile(w)
またはwhile(x2)
を読むことになっていました。私はそのファイルの歴史の中から作者を見つけて「WTF」Eメールを送った方がいいでしょう…そして今私は自分の精神的な文脈を壊しました。 while(2)
は私の時間の数分を消費するかもしれません、while(1)
はほんの一瞬でかかりました!
誇張ですが、ほんの少しです。コードの読みやすさは本当に重要です。そしてそれはインタビューで言及する価値があります!
特定のオプションセットを持つ特定のターゲット用に特定のコンパイラによって生成されたコードを示す既存の回答は、その質問に完全には答えていません。デフォルトのオプションでは? "など)。
言語の定義に関する限り、抽象マシンwhile (1)
は整数定数1
を評価し、while (2)
は整数定数2
を評価します。どちらの場合も、結果は等しいかどうかゼロで比較されます。言語標準は、2つの構成要素の相対的なパフォーマンスについては何も言っていません。
少なくとも最適化を要求せずにコンパイルされた場合、非常に素朴なコンパイラが2つの形式に対して異なるマシンコードを生成する可能性があると想像できます。
一方、Cコンパイラは、定数式を必要とするコンテキストに現れる場合、コンパイル時にsome定数式を絶対に評価する必要があります。たとえば、
int n = 4;
switch (n) {
case 2+2: break;
case 4: break;
}
診断が必要です。怠惰なコンパイラは実行時まで2+2
の評価を延期するオプションを持っていません。コンパイラはコンパイル時に定数式を評価する能力を持たなければならないので、それが必要でないときでさえその能力を利用しない理由はありません。
C規格( N1570 6.8.5p4)によると、
反復ステートメントは、制御式が0に等しくなるまでループ本体というステートメントを繰り返し実行します。
したがって、関連する定数式は1 == 0
と2 == 0
であり、どちらもint
値0
に評価されます。 (これらの比較はwhile
ループのセマンティクスに暗黙的に含まれています。実際のC式としては存在しません。)
逆に素朴なコンパイラcouldは、2つの構成要素に対して異なるコードを生成します。たとえば、最初の場合は無条件無限ループを生成し(1
を特別な場合として扱う)、2番目の場合は2 != 0
と同等の明示的な実行時比較を生成します。しかし、実際にそのように動作するCコンパイラに遭遇したことは一度もありません。そのようなコンパイラが存在することに真剣に疑います。
ほとんどのコンパイラ(私はすべての製品品質のコンパイラを言うつもりです)には、追加の最適化を要求するオプションがあります。そのようなオプションの下では、どのコンパイラも2つの形式に対して異なるコードを生成する可能性はさらに低くなります。
コンパイラが2つの構成に対して異なるコードを生成する場合は、まず、異なるコードシーケンスが実際に異なるパフォーマンスを持っているかどうかを確認してください。もしそうならば、最適化オプション(もし可能であれば)を使用して再度コンパイルしてみてください。それでも異なる場合は、バグ報告をコンパイラの製造元に送信してください。 C規格に準拠していないという意味では必ずしもバグではありませんが、ほぼ確実に修正すべき問題です。
結論:while (1)
とwhile(2)
ほぼは確かに同じパフォーマンスを持ちます。それらはまったく同じセマンティクスを持ち、どのコンパイラも同一のコードを生成しない理由はありません。
while(1)
よりwhile(2)
の方が速いコードを生成するのはコンパイラにとって完全に合法ですが、同じプログラム内でwhile(1)
が出現するのよりもwhile(1)
の方が速いコードを生成するのはコンパイラにとって同様に合法です。
(あなたが尋ねた質問には、別の疑問が含まれています。間違った技術的な主張を主張するインタビュアーにどのように対処しますか。 Workplaceサイト にとってはおそらく良い質問でしょう)。
ちょっと待って。インタビュアー、彼はこの男のように見えましたか?
インタビュアー自身がこのインタビューに失敗したことは十分に悪いことですこの会社の他のプログラマーがこのテストに「合格」した場合はどうでしょうか?
いいえ。ステートメント1 == 0
および2 == 0
の評価は同様に高速でなければなりません。私たちはを想像することができます一方が他方よりも高速である可能性のあるコンパイラの実装が貧弱です。しかし、一方が他方より速くなければならないgood理由はありません。
主張が真実である場合にあいまいな状況があったとしても、プログラマーはあいまいな(この場合は気味悪い)トリビアの知識に基づいて評価されるべきではありません。このインタビューについて心配する必要はありません。ここでの最善の策は、立ち去ることです。
免責事項:これはオリジナルのディルバートの漫画ではありません。これは単に mashup 。
あなたの説明は正しいです。これは技術的な知識に加えてあなたの自信をテストする質問のようです。
ところで、あなたが答えたら
どちらも完了までに時間がかかるため、どちらのコードも同じくらい高速です。
インタビュアーは言うだろう
しかし
while (1)
は1秒あたりの反復回数を増やすことができます。なぜあなたは説明できますか? (これはナンセンスです。もう一度あなたの自信をテストしてください)
だからあなたがしたように答えることによって、あなたはさもなければこの悪い質問を議論するのに無駄になるであろう時間を節約した。
これが私のシステム(MS Visual Studio 2012)上でコンパイラによって生成されたコード例で、最適化はオフになっています。
yyy:
xor eax, eax
cmp eax, 1 (or 2, depending on your code)
je xxx
jmp yyy
xxx:
...
最適化をオンにした場合
xxx:
jmp xxx
そのため、生成されたコードは、少なくとも最適化コンパイラではまったく同じです。
質問に対する最も可能性の高い説明は、プロセッサが数字の個々のビットをゼロでない値に達するまで1つずつチェックするとインタビュアーが考えることです。
1 = 00000001
2 = 00000010
「ゼロか」アルゴリズムは数の右側から始めて、それがゼロでないビットに達するまで各ビットをチェックしなければなりません、while(1) { }
ループは反復ごとにwhile(2) { }
ループの2倍のビットをチェックしなければならないでしょう。
これは、コンピュータがどのように機能するかについての非常に誤った精神モデルを必要としますが、独自の内部ロジックを持っています。確認する1つの方法は、while(-1) { }
またはwhile(3) { }
が同じくらい速いかどうか、またはwhile(32) { }
が さらに遅い かどうかを尋ねることです。
もちろん、私はこのマネージャーの本当の意図を知りませんが、まったく異なる見解を提案します。新しいメンバーをチームに雇用するとき、彼がどのように対立状況に反応するかを知ることは有用です。
彼らはあなたを紛争に駆り立てた。これが本当なら、彼らは賢いと質問は良かった。銀行業のようないくつかの業界では、あなたの問題をStack Overflowに投稿することが拒否の理由になるかもしれません。
しかし、もちろん私は知りません、私はただ一つの選択肢を提案します。
手がかりは「上級管理者からの質問」にあると思います。この人は明らかにマネージャーになったときにプログラミングを停止し、シニアマネージャーになるには数年かかりました。プログラミングに興味を失ったことはありませんが、当時から行を書いたことはありません。そのため、彼の言及は、いくつかの答えが言及しているような「そこにあるまともなコンパイラー」ではなく、「この人が20〜30年前に働いたコンパイラー」です。
当時、プログラマーはかなりの割合の時間を費やして、「中央のミニコンピューター」のCPU時間は非常に価値があったため、コードをより高速かつ効率的にするためのさまざまな方法を試しました。コンパイラを書いている人たちもそうでした。当時彼の会社が提供していた唯一のコンパイラは、「頻繁に遭遇する最適化可能なステートメント」に基づいて最適化されており、しばらく遭遇したときに少しのショートカットを取り、すべてを評価したと推測していますその他(while(2)を含む)。このような経験をしたことで、彼の立場とそれに対する自信を説明することができました。
あなたを雇うための最良のアプローチは、おそらくシニアマネージャーが夢中になって、あなたの前に「プログラミングの古き良き時代」について2〜3分あなたに講義することを可能にするものですスムーズにインタビュー対象。 (ここではタイミングを合わせることが重要です-速すぎてストーリーを中断しています-遅すぎて、フォーカスが不十分な人物としてラベル付けされています)。インタビューの最後に、このトピックについてもっと知りたいと強く思うと伝えてください。
あなたは彼にその結論にどのように到達したのか彼に尋ねたはずです。そこにあるまともなコンパイラの下では、2つは同じasm命令にコンパイルされます。だから、彼はあなたにコンパイラーにも始めるように言ったはずです。たとえそうだとしても、理論的な知識を持って推測するには、コンパイラとプラットフォームを非常によく知っている必要があります。そして最後に、それは実際には重要ではありません。ループに影響を与えるであろうメモリの断片化やシステム負荷のような他の外部要因がこの詳細よりも多いからです。
この質問のために、C委員会のDoug Gwynが、オプティマイザパスなしの初期のCコンパイラのいくつかがwhile(1)
のテストを生成すると書いていたことを覚えておいてください(for(;;)
はそうではありません)。
私はこの歴史的なメモを書いてインタビュアーに答え、そしてたとえ私が非常に驚いたとしても、どんなコンパイラーがこれをしたとしても、コンパイラーは以下のことをすることができると言います。
while(1)
とwhile(2)
の両方のテストを生成するwhile(1)
を慣用句と見なすので、(無条件ジャンプで)最適化するように指示されます。これはwhile(2)
にテストを残すことになり、そのため両者の間にパフォーマンスの違いが生じます。while(1)
とwhile(2)
を同じ構文と見なさないことは、これらが同等の構文であるため、低品質の最適化の兆候であることをインタビュアーに追加します。
そのような質問に対する別の見方は、あなたが上司が間違っていることを上司に伝える勇気があるかどうかを確かめることです。そして、あなたはそれをどれほどゆるやかに伝えることができます。
私の最初の本能は、まともなコンパイラがそれを処理しなければならないことをマネージャに示すためにアセンブリ出力を生成することでした。もしそうでなければ、次のパッチを提出するでしょう:)
非常に多くの人がこの問題を詳しく調べてみるために、これがどれほど早く マイクロ最適化 物事を望んでいるかを確かめるためのテストになり得る理由を正確に示しています。
私の答えはそうでしょう。それはそれほど問題ではありません、私はむしろ解決しているビジネス上の問題に焦点を合わせます。結局のところ、それは私が支払うつもりですものです。
さらに、私はwhile(1) {}
を選択するのがより一般的であり、他のチームメイトがなぜ1よりも高い数字を求めるのかを理解するために時間を費やす必要はないでしょう。
それではいくつかのコードを書きましょう。 ;-)
私にはこれは技術的な質問として隠されている行動面接の質問の1つです。何人かの会社はこれをします - 彼らはどんな有能なプログラマーにも答えることがかなり容易であるべきである技術的な質問をするでしょう、しかしインタビュイーが正しい答えを与えるとき、インタビュアーは彼らに彼らが間違っていると言います。
会社はあなたがこの状況でどう反応するか見たいと思っています。あなたは静かにそこに座り、あなたの答えが正しいとプッシュしないのですか。それは自信がないか、インタビュアーを動揺させる恐れがあるからです。それともあなたが間違っていることを知っている権威のある人に挑戦しても構わないと思いますか?彼らはあなたがあなたの信念に立ち向かうことをいとわないかどうか、そしてあなたがそれを巧妙で敬意を持って行うことができるかどうかを見たがっています。
最適化が心配な場合は、次のものを使用してください。
for (;;)
それはテストがないからです。 (シニックモード)
この種のナンセンスが違いを生むかもしれないとき、私はCとアセンブリコードをプログラムし直しました。違いが出たときは、議会に書きました。
私がその質問をされたならば、私は時期尚早の最適化についてドナルド・クヌースの有名な1974年の引用を繰り返して、インタビュアーが笑って動かなかったなら歩きました。
インタビュアーがそのような愚かな質問を意図的に投げかけて、あなたに3つの点を指摘してほしいのかもしれません。
両者は等しい - 同じです。
仕様によると、0ではないものはすべて真と見なされ、最適化が行われていなくても、while(1)またはwhile(2)のコードは生成されません。コンパイラは!= 0
の単純なチェックを生成します。
問題があります。実際にプログラムを作成してその速度を測定すると、両方のループの速度が異なる可能性があります。いくつかの合理的な比較のために:
unsigned long i = 0;
while (1) { if (++i == 1000000000) break; }
unsigned long i = 0;
while (2) { if (++i == 1000000000) break; }
時間を表示するコードが追加されていると、1〜2キャッシュライン内でループがどのように配置されるかのようなランダムな効果が生じる可能性があります。 1つのループが完全に1つのキャッシュライン内、またはキャッシュラインの最初にある場合、または2つのキャッシュラインにまたがる場合があります。そして結果として、インタビュアーが最も速いと主張するものは、実際には最も速いかもしれません - 偶然の一致による。
最悪の場合のシナリオ:最適化コンパイラは、ループの動作を理解していませんが、2番目のループが実行されたときに生成される値は最初のループで生成される値と同じであると判断します。そして、最初のループのために完全なコードを生成しますが、2番目のためには生成しません。
このまっすぐな質問Idをテストし、証明し、そして答えるために人々が費やした時間と努力の量から判断すると、両方とも質問をすることによって非常に遅くされたと言います。
そしてもっと時間をかけて...
"while(2)"はばかげている、
"while(1)"と "while(true)"は歴史的には、必ず発生する条件に基づいて、ループ内のある段階で "break"が呼び出されることを期待する無限ループを作成するために使用されます。
"1"は単に真に評価するためだけに存在します。したがって、 "while(2)"は "while(1 + 1 == 2)"と同じくらい愚かに言っても真に評価されます。
そして、あなたが完全に愚かになりたい場合は単に使用します -
while (1 + 5 - 2 - (1 * 3) == 0.5 - 4 + ((9 * 2) / 4)) {
if (succeed())
break;
}
あなたのコーダーがコードの実行に影響を及ぼさないタイプミスをしたと思うのですが、彼が意図的に "2"を奇妙に使うために使用した場合読んで一緒に働く。
それはコンパイラによって異なります。
コードを最適化する場合、または特定の命令セットに対して同じ数の命令を使用して1と2を真と評価する場合、実行速度は同じになります。
実際の場合は、常に同じ速度で実行されますが、評価方法が異なる場合は、特定のコンパイラと特定のシステムを想像することも可能です。
つまり、これは実際には言語(C)に関連した質問ではありません。
明らかな答えは次のとおりです。投稿されたように、両方のフラグメントは等しく忙しい無限ループを実行し、プログラムを無限に slow にします。
Cキーワードをマクロとして再定義すると、技術的には未定義の動作になりますが、どちらかのコードを高速化することができる唯一の方法です。この2行の上に次の行を追加できます。
#define while(x) sleep(x);
それは確かにwhile(1)
をwhile(2)
の2倍の速さ(または半分の遅さ)にするでしょう。
この質問に答えようとする人々は最速のループを望んでいるので、他の答えで述べたように、私は両方が同じアセンブリコードに等しくコンパイルされていると答えました。それでも、whileループの代わりに'loop unrolling'; a do {} whileループ _を使用してインタビュアーに提案することができます。
気をつけて:あなたはループが少なくとも常に実行されることを確認する必要があります。
ループの内部にはブレーク条件があります。
また、そのようなループでは、0以外の任意の整数がその仕事をするので、私は個人的にdo {} while(42)の使用を好みます。