web-dev-qa-db-ja.com

初期化されていないローカル変数は最速の乱数発生器ですか?

初期化されていないローカル変数は未定義の動作であることがわかっています(UB)。また、値にトラップ表現が含まれている可能性がありますが、これはさらなる操作に影響する可能性があります視覚的な表現にのみ乱数を使用し、プログラムの他の部分ではそれらをさらに使用しないようにします。たとえば、視覚効果にランダムな色を設定します。たとえば:

void updateEffect(){
    for(int i=0;i<1000;i++){
        int r;
        int g;
        int b;
        star[i].setColor(r%255,g%255,b%255);
        bool isVisible;
        star[i].setVisible(isVisible);
    }
}

それよりも速いですか

void updateEffect(){
    for(int i=0;i<1000;i++){
        star[i].setColor(Rand()%255,Rand()%255,Rand()%255);
        star[i].setVisible(Rand()%2==0?true:false);
    }
}

また、他の乱数ジェネレーターよりも高速ですか?

315
ggrr

他の人が指摘したように、これは未定義の動作(UB)です。

実際には、(おそらく)実際に(種類の)動作します。 x86 [-64]アーキテクチャで初期化されていないレジスタから読み取ると、実際にはガベージ結果が生成され、おそらく悪いことは行われません(たとえば、Itaniumとは対照的に、 レジスタに無効のフラグを立てることができます 読み取りはNaNのようなエラーを伝播します)。

ただし、主に2つの問題があります。

  1. それは特にランダムではありません。この場合、あなたはスタックから読んでいるので、以前にそこにあったものを取得します。これは、10分前に入力したパスワード、または祖母のクッキーレシピなど、事実上ランダムで完全に構造化されている可能性があります。

  2. これは悪い(大文字の 'B')このようなことをコードに忍び込ませる練習です。技術的には、コンパイラは未定義の変数を読み取るたびにreformat_hdd();を挿入できます。それはしませんが、とにかくすべきではない。安全でないことをしないでください。例外を少なくすればするほど、偶発的なミスallから安全になります。

    UBのより緊急な問題は、プログラム全体の動作が未定義になることです。最新のコンパイラは、これを使用してコードの膨大なスワスを削除することもできますし、 過去に戻る を削除することもできます。 UBで遊ぶことは、生きている原子炉を解体するビクトリア朝のエンジニアのようなものです。うまくいかないことは無数にあります。おそらく、基本的な原理や実装されたテクノロジーの半分を知らないでしょう。 mightでも大丈夫ですが、それを起こさないでください。詳細については、他のニースの回答をご覧ください。

また、私はあなたを解雇します。

295
imallett

これをはっきり言ってみましょう:私たちはプログラムで未定義の動作を呼び出しません。決して良いアイデアとは言えません。この規則にはまれな例外があります。たとえば、 offsetofを実装するライブラリ実装者 の場合。あなたのケースがそのような例外に該当する場合、あなたはおそらくすでにこれを知っています。この場合、 初期化されていない自動変数の使用は未定義の動作であることがわかります

コンパイラは、未定義の動作に関する最適化により非常に積極的になっており、未定義の動作がセキュリティの欠陥につながる多くのケースを見つけることができます。最も悪名高いケースはおそらく Linuxカーネルのヌルポインターチェックの削除C++コンパイルのバグに対する私の答え? で定義されていますが、未定義の動作に関するコンパイラーの最適化により有限ループになりました無限のもの。

CERTの 危険な最適化と因果関係の喪失ビデオ )を読むことができます。

コンパイラ作成者は、CおよびC++プログラミング言語の未定義の動作を利用して、最適化を改善しています。

多くの場合、これらの最適化は、開発者がソースコードに対して原因分析、つまり、ダウンストリームの結果の以前の結果への依存性を分析する能力を妨げています。

その結果、これらの最適化はソフトウェアの因果関係を排除し、ソフトウェアの障害、欠陥、および脆弱性の可能性を高めています。

特に不定値に関しては、C標準 欠陥レポート451:初期化されていない自動変数の不安定性 が興味深い読み物になります。まだ解決されていませんが、wobbly valuesの概念が導入されています。これは、値の不確定性がプログラム全体に伝播し、プログラムのさまざまなポイントでさまざまな不確定値を持つ可能性があることを意味します。

これが発生する例は知りませんが、現時点では除外できません。

期待どおりの結果ではなく、実際の例

ランダムな値を取得することはほとんどありません。コンパイラはループを完全に最適化できます。たとえば、この単純化されたケースでは:

void updateEffect(int  arr[20]){
    for(int i=0;i<20;i++){
        int r ;    
        arr[i] = r ;
    }
}

clangはそれを最適化します(ライブで見る):

updateEffect(int*):                     # @updateEffect(int*)
    retq

または、この修正されたケースのように、すべてゼロを取得することもできます。

void updateEffect(int  arr[20]){
    for(int i=0;i<20;i++){
        int r ;    
        arr[i] = r%255 ;
    }
}

ライブで見る

updateEffect(int*):                     # @updateEffect(int*)
    xorps   %xmm0, %xmm0
    movups  %xmm0, 64(%rdi)
    movups  %xmm0, 48(%rdi)
    movups  %xmm0, 32(%rdi)
    movups  %xmm0, 16(%rdi)
    movups  %xmm0, (%rdi)
    retq

これらの両方のケースは、未定義の動作の完全に受け入れられる形式です。

Itaniumを使用している場合、 トラップ値で終わる

[...]レジスタがたまたま特別なnot-a-thing値を保持している場合、いくつかの命令を除いてレジスタトラップを読み取ります[...]

その他の重要な注意事項

B Canariesプロジェクトで指摘されているgccとclangの差異 初期化されていないメモリに関して未定義の動作を利用する意思があることに注意してください。記事ノート(emphasis mine):

もちろん、そのような期待は言語標準と特定のコンパイラーが行うこととは関係がないことを完全に明確にする必要があります。そのコンパイラーのプロバイダーはそのUBまたは単にそれを悪用しようとしていないためです。コンパイラプロバイダーからの実際の保証が存在しない場合、未使用のUBは時限爆弾であると言いたいです:彼らは次に出かけるのを待っていますコンパイラがもう少し積極的になる月または来年。

Matthieu M.が指摘しているように、 すべてのCプログラマーが未定義の動作#2/3について知っておくべきこと もこの質問に関連しています。とりわけ、(emphasis mine):

気付くべき重要で恐ろしいことは、未定義の振る舞いに基づいた任意の最適化が、バグのあるコードでいつでもトリガーされ始める可能性があることですfuture。インライン化、ループの展開、メモリの昇格、その他の最適化は改善され続け、既存の理由の重要な部分は、上記のような二次的な最適化を公開することです。

コンパイラーが必然的に非難されることもありますが、それはCコードの巨大な本体が地雷がただ爆発するのを待っていることも意味するため、これは非常に不満です。

完全を期すために、実装では未定義の動作を適切に定義することを選択できることをたぶん言及する必要があります。たとえば、 gccはユニオンを介した型のパニングを許可します while C++ではこれは未定義の動作のようです これが事実である場合、実装はそれを文書化する必要があり、これは通常移植性がありません。

196
Shafik Yaghmour

いいえ、ひどいです。

初期化されていない変数を使用する動作は、CとC++の両方で未定義であり、そのようなスキームが望ましい統計特性を持つことはほとんどありません。

「高速で汚れた」乱数ジェネレータが必要な場合は、Rand()が最善の策です。その実装では、乗算、加算、およびモジュラスのみが実行されます。

私が知っている最速のジェネレーターでは、擬似乱数変数Iの型としてuint32_tを使用し、

I = 1664525 * I + 1013904223

連続した値を生成します。あなたの空想を取るIの初期値(seedと呼ばれる)を選択できます。明らかに、インラインでコーディングできます。符号なしタイプの標準保証ラップアラウンドがモジュラスとして機能します。 (数値定数は、その注目に値する科学プログラマーのドナルドクヌースによって厳選されています。)

162
Bathsheba

良い質問!

未定義は、ランダムであることを意味しません。考えてみてください。グローバルな初期化されていない変数で取得する値は、システムまたは実行中のアプリケーションによって残されたものです。使用されていないメモリでシステムが何をするか、および/またはシステムとアプリケーションがどのような値を生成するかに応じて、以下を取得できます。

  1. いつも同じ。
  2. 値の小さなセットの1つである。
  3. 1つ以上の小さな範囲の値を取得します。
  4. 16/32/64ビットシステムのポインターから2/4/8で分割可能な多くの値を表示
  5. ...

取得する値は、システムやアプリケーションによって残された非ランダム値に完全に依存します。したがって、確かにノイズが発生します(システムがメモリを使用しなくなった場合を除きます)が、描画元の値プールは決してランダムではありません。

ローカル変数は、プログラムのスタックから直接取得されるため、事態はさらに悪化します。他のコードの実行中にプログラムがこれらのスタック位置を実際に書き込む可能性は非常に高いです。この状況での運の可能性は非常に低く、あなたが行う「ランダムな」コード変更はこの運を試してみます。

randomness について読んでください。ご覧のとおり、ランダム性は非常に具体的であり、プロパティを取得するのは困難です。追跡するのが難しいもの(提案など)を取得した場合、ランダムな値を取得すると考えるのはよくある間違いです。

42
meaning-matters

多くの良い答えがありますが、決定論的なコンピューターではランダムなものは何もないという点を強調します。これは、疑似RNGによって生成される数値と、スタック上のC/C++ローカル変数用に予約されたメモリ領域にある「ランダムな」数値の両方に当てはまります。

しかし...決定的な違いがあります。

優れた擬似ランダムジェネレーターによって生成された数値には、真のランダムドローと統計的に類似した特性があります。たとえば、分布は均一です。サイクルの長さが長い:サイクルが繰り返される前に、数百万の乱数を取得できます。シーケンスは自己相関ではありません。たとえば、2番目、3番目、または27番目の数字をすべて取得した場合、または生成された数字の特定の数字を調べた場合に、奇妙なパターンが現れ始めることはありません。

対照的に、スタックに残された「乱数」にはこれらのプロパティはありません。それらの値と見かけのランダム性は、プログラムの構築方法、コンパイル方法、およびコンパイラによる最適化方法に完全に依存します。例として、自己完結型プログラムとしてのアイデアのバリエーションを次に示します。

#include <stdio.h>

notrandom()
{
        int r, g, b;

        printf("R=%d, G=%d, B=%d", r&255, g&255, b&255);
}

int main(int argc, char *argv[])
{
        int i;
        for (i = 0; i < 10; i++)
        {
                notrandom();
                printf("\n");
        }

        return 0;
}

LinuxマシンでGCCを使用してこのコードをコンパイルして実行すると、かなり不快な決定論的であることがわかりました。

R=0, G=19, B=0
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255

逆アセンブラでコンパイルされたコードを見ると、何が起こっているかを詳細に再構築できます。 notrandom()の最初の呼び出しは、以前にこのプログラムで使用されなかったスタックの領域を使用しました。誰がそこにあったかを知っています。しかし、notrandom()への呼び出しの後、printf()への呼び出しがあり(GCCコンパイラは実際にputchar()への呼び出しに最適化されますが、気にしません)、それはスタックを上書きします。したがって、次回以降、notrandom()が呼び出されると、スタックにはputchar()の実行による古いデータが含まれます。また、putchar()は常に同じ引数で呼び出されるため、この古いデータは常に同じになります。も。

したがって、この振る舞いについては絶対にnothing randomであり、この方法で得られた数値は、適切に記述された擬似乱数ジェネレーターの望ましい特性を備えていません。実際、ほとんどの実際のシナリオでは、それらの値は反復的で高度に相関しています。

実際、他の人と同様に、私はこのアイデアを「高性能RNG」として偽装しようとした人を解雇することも真剣に考えています。

32
Viktor Toth

未定義の動作とは、コンパイラの作成者が問題を無視する自由があることを意味します。プログラマーは、何が起こっても不平を言う権利を決して持たないからです。

理論的にはUBランドに入るとき何でも起こり得る(鼻から飛ぶ デーモンを含む )通常意味するのは、コンパイラの作者が気にしないことであり、 、ローカル変数の場合、値はその時点でスタックメモリにあるものになります。

これはまた、コンテンツが「奇妙」であるが、固定またはわずかにランダムまたは可変であるが、明確なパターン(たとえば、各反復で値が増加する)を持つことを意味します。

確かにcannotそれがまともなランダムジェネレーターであることを期待してください。

29
6502

未定義の動作は未定義です。未定義の値を取得することを意味するのではなく、プログラムがanythingを実行し、それでも言語仕様を満たすことができることを意味します。

適切な最適化コンパイラは

void updateEffect(){
    for(int i=0;i<1000;i++){
        int r;
        int g;
        int b;
        star[i].setColor(r%255,g%255,b%255);
        bool isVisible;
        star[i].setVisible(isVisible);
    }
}

そして、noopにコンパイルします。これは他のどの方法よりも確かに高速です。それは何もしないという欠点がありますが、それは未定義の振る舞いの欠点です。

28
Martijn

セキュリティ上の理由により、プログラムに割り当てられた新しいメモリを消去する必要があります。そうしないと、情報が使用され、パスワードがアプリケーション間で漏洩する可能性があります。メモリを再利用する場合にのみ、0以外の値を取得します。また、スタック上では、そのメモリの以前の使用が修正されるため、以前の値が修正される可能性が非常に高くなります。

18
Arne

まだ言及されていませんが、未定義の動作を呼び出すコードパスは、コンパイラが望むものを何でもすることができます。

void updateEffect(){}

これは正しいループよりも確かに高速であり、UBのために完全に適合しています。

18
Caleth

あなたの特定のコード例は、おそらくあなたが期待していることをしないでしょう。技術的には、ループの各反復でr、g、およびbの値のローカル変数が再作成されますが、実際にはスタック上のまったく同じメモリスペースです。したがって、反復ごとに再ランダム化されることはなく、r、g、bの個別のランダム性や初期のランダム性に関係なく、1000色ごとに同じ3つの値を割り当てることになります。

確かに、それが機能する場合、私はそれを再ランダム化するものについて非常に興味があります。私が考えることができる唯一のものは、そのスタックの上にピギーパックされたインターリーブ割り込みです。特に、可視性の設定が特にレジスターを必要とする場合、ループ内でレジスターを再利用する真のメモリー位置としてではなく、レジスター変数としてそれらを保持する内部最適化もうまくいくでしょう。それでも、ランダムからはほど遠い。

13
Jos

ここでほとんどの人が未定義の動作に言及したように。未定義は、有効な整数値を(幸運にも)取得する可能性があることも意味し、この場合は(Rand関数呼び出しが行われないため)高速になります。しかし、実際には使用しないでください。運はいつもあなたと一緒ではないので、これは恐ろしい結果になると確信しています。

12
Ali Kazmi

すごく悪い!悪い習慣、悪い結果。考慮してください:

A_Function_that_use_a_lot_the_Stack();
updateEffect();

関数A_Function_that_use_a_lot_the_Stack()が常に同じ初期化を行うと、スタックには同じデータが残ります。そのデータは、updateEffect()常に同じ値!を呼び出すことで得られます。

12
Frankie_C

非常に簡単なテストを実行しましたが、ランダムではありませんでした。

#include <stdio.h>

int main() {

    int a;
    printf("%d\n", a);
    return 0;
}

プログラムを実行するたびに、同じ番号(私の場合は32767)が出力されました。それよりもずっとランダムになることはありません。これはおそらく、ランタイムライブラリのスタートアップコードがスタックに残したものです。プログラムが実行されるたびに同じ起動コードが使用され、実行ごとにプログラムが異なることはないため、結果は完全に一貫しています。

11
Barmar

「ランダム」の意味を定義する必要があります。賢明な定義には、取得する値にほとんど相関がないことが含まれます。それはあなたが測定できるものです。また、制御された再現可能な方法で達成することは簡単ではありません。したがって、未定義の動作は確かにあなたが探しているものではありません。

10
Zsolt Szatmari

「unsigned char *」タイプを使用して、初期化されていないメモリを安全に読み取ることができる特定の状況があります[例: malloc]から返されたバッファ。コードは、コンパイラがウィンドウから因果関係をスローすることを心配せずにそのようなメモリを読み取ることができます。また、初期化されていないデータが読み取られないことを保証するよりも、メモリに含まれる可能性のあるものに対してコードを準備する方が効率的である場合があります(この一般的な例は、意味のあるデータを含むすべての要素を個別にコピーするのではなく、部分的に初期化されたバッファでmemcpyを使用することです。

ただし、そのような場合でも、バイトの組み合わせが特に厄介な場合、それを読み取ると常にそのバイトのパターンが生成されると仮定する必要があります(そして特定のパターンが生産では厄介であるが開発ではない場合、パターンは、コードが実稼働するまで表示されません)。

初期化されていないメモリの読み取りは、システムに最後に電源が投入されて以来、メモリに実質的に非ランダムなコンテンツが書き込まれていないことを確認できる組み込みシステムのランダム生成戦略の一部として役立ちます。メモリに使用されるプロセスにより、電源投入時の状態が半ランダムに変化します。すべてのデバイスが常に同じデータを生成する場合でも、コードは機能するはずですが、たとえばノードのグループはそれぞれ、任意の一意のIDを可能な限り迅速に選択する必要があります。ノードの半分に同じ初期IDを与える「あまりランダムではない」ジェネレーターを使用すると、ランダム性の初期ソースをまったく持たないよりも優れている場合があります。

7
supercat

他の人が言ったように、それは高速ですが、ランダムではありません。

ほとんどのコンパイラがローカル変数に対して行うことは、スタック上の変数のためにいくらかのスペースを確保することですが、何も設定する必要はありません(標準では必要ないので、生成するコードを遅くするのはなぜですか?).

この場合、取得する値は、以前にスタック上にあったものに依存します-この関数の前に、100個のローカルchar変数がすべて「Q」に設定されている関数を呼び出し、その後関数を呼び出す場合返されると、おそらく「ランダムな」値がmemset()をすべて「Q」にしたかのように振る舞うことになります。

重要なのは、これを使用しようとしているサンプル関数では、これらの値は読むたびに変化することはなく、毎回同じになることです。したがって、100個の星がすべて同じ色と可視性に設定されます。

また、コンパイラがこれらの値を初期化するべきではないということは何もありません。したがって、将来のコンパイラは初期化するかもしれません。

一般的に:悪い考え、それをしないでください。 (多くの「賢い」コードレベルの最適化のように...)

5
Alun Thomas

初期化されていない変数を使用したいすべての場所で7757を使用します。素数のリストからランダムに選択しました。

  1. 定義された動作です

  2. 常に0であるとは限りません

  3. 素数です

  4. 初期化されていない変数と同じくらい統計的にランダムである可能性が高い

  5. コンパイル時に値がわかっているため、初期化されていない変数よりも高速である可能性が高い

3

Not言語の未定義の振る舞いにロジックを頼るのは良い考えです。この投稿で言及/議論されたものに加えて、現代のC++アプローチ/スタイルでは、そのようなプログラムはコンパイルできない可能性があることに言及したいと思います。

これは、auto機能の利点とその便利なリンクを含む以前の投稿で言及されました。

https://stackoverflow.com/a/26170069/27247

したがって、上記のコードを変更し、実際の型をautoに置き換えると、プログラムはコンパイルされません。

void updateEffect(){
    for(int i=0;i<1000;i++){
        auto r;
        auto g;
        auto b;
        star[i].setColor(r%255,g%255,b%255);
        auto isVisible;
        star[i].setVisible(isVisible);
    }
}
3
Mantosh Kumar

他の人がすでに言及したように、これは未定義の動作(UB)ですが、「機能する」可能性があります。

他の人が既に言及した問題を除いて、もう1つの問題(欠点)があります-CとC++以外の言語では動作しません。この質問はC++についてのものであることは知っていますが、C++とJavaの両方に適したコードを書くことができ、それが問題にならないのなら、なぜですか?たぶんいつか誰かがそれを他の言語に移植し、 "手品" このようなUBは間違いなく悪夢です(特に経験の浅いC/C++開発者にとって)。

ここ 他の同様のUBについての質問があります。このUBを知らずに、このようなバグを見つけようとしている自分を想像してください。 C/C++でこのような奇妙なことについてもっと知りたい場合は、リンクから質問の答えを読んで thisGREATスライドショー。これは、内部にあるものとその仕組みを理解するのに役立ちます。 「魔法」に満ちたスライドショーだけではありません。経験豊富なC/c ++プログラマーのほとんどでさえ、これから多くを学ぶことができると確信しています。

3
cyriel

あなたの考え方が好きです。本当に箱の外。ただし、トレードオフは実際に価値がありません。 メモリとランタイムのトレードオフは、ランタイムの未定義の動作を含むnotです。

ビジネスロジックとしてこのような「ランダム」を使用していることを知ると、非常に不安な気持ちになるはずです。しませんでした。

3
DDan

考慮すべきもう1つの可能性があります。

最新のコンパイラ(ahem g ++)は非常にインテリジェントであるため、コードを調べて、どの命令が状態に影響を与え、どの命令が状態に影響を与えないかを確認します。

それで、ここで何が起こるでしょう。 g ++は、あなたが読んでいる、算術演算をしている、保存している、本質的にはより多くのゴ​​ミを生成するゴミの値を確実に認識します。新しいガベージが古いガベージよりも有用であるという保証はないため、ループは単純に廃止されます。 BLOOP!

この方法は便利ですが、これが私がやることです。 UB(未定義の動作)とRand()の速度を組み合わせます。

もちろん、実行されるRand()sを減らしますが、それらを混ぜて、コンパイラが望まないことをしないようにします。

そして、私はあなたを解雇しません。

1
ps95