シングルトンのスレッドセーフな初期化

シングルトン パターンには多くの問題があります。私はそれを完全に認識しています。しかし、シングルトン パターンは変数の理想的なユース ケースであり、スレッド セーフな方法で初期化するだけで済みます。その時点から、同期せずに使用できます。そのため、この投稿では、マルチスレッド環境でシングルトンを初期化するさまざまな方法について説明します。パフォーマンスの数値を取得し、スレッドセーフな変数の初期化のユース ケースについて推論できます。

C++11 でシングルトンをスレッドセーフな方法で初期化するには、さまざまな方法があります。概観すると、C++ ランタイム、ロック、またはアトミックからの保証を得ることができます。パフォーマンスへの影響について非常に興味があります。

私の戦略

パフォーマンス測定の基準点として、4,000 万回シーケンシャル アクセスしたシングルトン オブジェクトを使用します。最初のアクセスでオブジェクトが初期化されます。対照的に、マルチスレッド プログラムからのアクセスは 4 つのスレッドによって行われます。ここでは、パフォーマンスにのみ関心があります。プログラムは 2 台の実際の PC で実行されます。 Linux PC には 4 つのコアがあり、Windows PC には 2 つのコアがあります。最適化なしで最大限にプログラムをコンパイルします。プログラムを最大限に最適化して変換するには、静的メソッド getInstance で揮発性変数を使用する必要があります。そうしないと、コンパイラがシングルトンへのアクセスを最適化しなくなり、プログラムが速すぎます。

3 つの質問があります:

<オール>
  • さまざまなシングルトン実装の相対的なパフォーマンスはどうですか?
  • Linux (gcc) と Windwos (cl.exe) に大きな違いはありますか?
  • 最適化されたバージョンと最適化されていないバージョンの違いは何ですか?
  • 最後に、すべての数値を表にまとめます。数字は秒単位です。

    基準値

    両方のコンパイラ

    コマンド ラインにコンパイラの詳細が表示されます。gcc と cl.exe は次のとおりです。

    参照コード

    まずはシングルスレッドのケース。もちろん、同期なしで。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    // singletonSingleThreaded.cpp
    
    #include <chrono>
    #include <iostream>
    
    constexpr auto tenMill= 10000000;
    
    class MySingleton{
    public:
     static MySingleton& getInstance(){
     static MySingleton instance;
     // volatile int dummy{};
     return instance;
     }
    private:
     MySingleton()= default;
     ~MySingleton()= default;
     MySingleton(const MySingleton&)= delete;
     MySingleton& operator=(const MySingleton&)= delete;
     
    };
    
    int main(){
     
     constexpr auto fourtyMill= 4* tenMill;
     
     auto begin= std::chrono::system_clock::now();
     
     for ( size_t i= 0; i <= fourtyMill; ++i){
     MySingleton::getInstance();
     }
     
     auto end= std::chrono::system_clock::now() - begin;
     
     std::cout << std::chrono::duration<double>(end).count() << std::endl;
    
    }
    

    参照実装では、いわゆる Meyers Singleton を使用します。この実装の洗練された点は、11 行目のシングルトン オブジェクト インスタンスがブロック スコープを持つ静的変数であることです。したがって、静的メソッド getInstance (行 10 ~ 14) が最初に実行されるときに、インスタンスは正確に初期化されます。 14 行目では、volatile 変数のダミーがコメント アウトされています。変更する必要がある最大の最適化でプログラムを翻訳するとき。したがって、呼び出し MySingleton::getInstance() は最適化されません。

    Linux と Windows で生の数値が表示されるようになりました。

    最適化なし

    最大限の最適化

    C++ ランタイムの保証

    変数のスレッド セーフな初期化の詳細については、スレッド セーフなデータの初期化の記事で既に説明しました。

    マイヤーズ シングルトン

    C++11 の Meyers Singleton の優れた点は、自動的にスレッドセーフであることです。これは標準によって保証されています:ブロックスコープを持つ静的変数。 Meyers Singleton はブロック スコープを持つ静的変数なので、これで完了です。 4 つのスレッド用にプログラムを書き直すことはまだ残っています。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    // singletonMeyers.cpp
    
    #include <chrono>
    #include <iostream>
    #include <future>
    
    constexpr auto tenMill= 10000000;
    
    class MySingleton{
    public:
     static MySingleton& getInstance(){
     static MySingleton instance;
     // volatile int dummy{};
     return instance;
     }
    private:
     MySingleton()= default;
     ~MySingleton()= default;
     MySingleton(const MySingleton&)= delete;
     MySingleton& operator=(const MySingleton&)= delete;
    
    };
    
    std::chrono::duration<double> getTime(){
    
     auto begin= std::chrono::system_clock::now();
     for ( size_t i= 0; i <= tenMill; ++i){
     MySingleton::getInstance();
     }
     return std::chrono::system_clock::now() - begin;
     
    };
    
    int main(){
     
     auto fut1= std::async(std::launch::async,getTime);
     auto fut2= std::async(std::launch::async,getTime);
     auto fut3= std::async(std::launch::async,getTime);
     auto fut4= std::async(std::launch::async,getTime);
     
     auto total= fut1.get() + fut2.get() + fut3.get() + fut4.get();
     
     std::cout << total.count() << std::endl;
    
    }
    

    関数 getTime でシングルトン オブジェクトを使用します (24 ~ 32 行目)。この関数は、36 行目から 39 行目の 4 つのプロミスによって実行されます。41 行目で関連先物の結果が合計されます。それだけです。実行時間だけが欠落しています。

    最適化なし

    最大限の最適化

    次のステップは、フラグ std::once_flag と組み合わせた関数 std::call_once です。

    関数 std::call_once とフラグ std::once_flag

    関数 std::call_once を使用して、1 回だけ実行される callable を登録できます。次の実装のフラグ std::call_once は、シングルトンがスレッドセーフに初期化されることを保証します。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    // singletonCallOnce.cpp
    
    #include <chrono>
    #include <iostream>
    #include <future>
    #include <mutex>
    #include <thread>
    
    constexpr auto tenMill= 10000000;
    
    class MySingleton{
    public:
     static MySingleton& getInstance(){
     std::call_once(initInstanceFlag, &MySingleton::initSingleton);
     // volatile int dummy{};
     return *instance;
     }
    private:
     MySingleton()= default;
     ~MySingleton()= default;
     MySingleton(const MySingleton&)= delete;
     MySingleton& operator=(const MySingleton&)= delete;
    
     static MySingleton* instance;
     static std::once_flag initInstanceFlag;
    
     static void initSingleton(){
     instance= new MySingleton;
     }
    };
    
    MySingleton* MySingleton::instance= nullptr;
    std::once_flag MySingleton::initInstanceFlag;
    
    std::chrono::duration<double> getTime(){
    
     auto begin= std::chrono::system_clock::now();
     for ( size_t i= 0; i <= tenMill; ++i){
     MySingleton::getInstance();
     }
     return std::chrono::system_clock::now() - begin;
     
    };
    
    int main(){
    
     auto fut1= std::async(std::launch::async,getTime);
     auto fut2= std::async(std::launch::async,getTime);
     auto fut3= std::async(std::launch::async,getTime);
     auto fut4= std::async(std::launch::async,getTime);
     
     auto total= fut1.get() + fut2.get() + fut3.get() + fut4.get();
     
     std::cout << total.count() << std::endl;
    
    }
    

    これが数字です。

    最適化なし

    最大限の最適化

    もちろん、最も明白な方法は、シングルトンをロックで保護することです。

    ロック

    ロックでラップされたミューテックスは、シングルトンがスレッドセーフに初期化されることを保証します。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    // singletonLock.cpp
    
    #include <chrono>
    #include <iostream>
    #include <future>
    #include <mutex>
    
    constexpr auto tenMill= 10000000;
    
    std::mutex myMutex;
    
    class MySingleton{
    public:
     static MySingleton& getInstance(){
     std::lock_guard<std::mutex> myLock(myMutex);
     if ( !instance ){
     instance= new MySingleton();
     }
     // volatile int dummy{};
     return *instance;
     }
    private:
     MySingleton()= default;
     ~MySingleton()= default;
     MySingleton(const MySingleton&)= delete;
     MySingleton& operator=(const MySingleton&)= delete;
    
     static MySingleton* instance;
    };
    
    
    MySingleton* MySingleton::instance= nullptr;
    
    std::chrono::duration<double> getTime(){
    
     auto begin= std::chrono::system_clock::now();
     for ( size_t i= 0; i <= tenMill; ++i){
     MySingleton::getInstance();
     }
     return std::chrono::system_clock::now() - begin;
     
    };
    
    int main(){
     
     auto fut1= std::async(std::launch::async,getTime);
     auto fut2= std::async(std::launch::async,getTime);
     auto fut3= std::async(std::launch::async,getTime);
     auto fut4= std::async(std::launch::async,getTime);
     
     auto total= fut1.get() + fut2.get() + fut3.get() + fut4.get();
     
     std::cout << total.count() << std::endl;
    }
    

    シングルトン パターンの従来のスレッド セーフな実装はどのくらい高速ですか?

    最適化なし

    最大限の最適化

    そんなに早くない。アトミックが違いを生むはずです。

    アトミック変数

    アトミック変数を使用すると、私の仕事は非常に困難になります。ここで、C++ メモリ モデルを使用する必要があります。よく知られたダブルチェック ロック パターンに基づいて実装しています。

    順次一貫性

    シングルトンへのハンドルはアトミックです。 C++ メモリ モデルを指定しなかったため、デフォルトが適用されます:順次整合性。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    // singletonAcquireRelease.cpp
    
    #include <atomic>
    #include <iostream>
    #include <future>
    #include <mutex>
    #include <thread>
    
    constexpr auto tenMill= 10000000;
    
    class MySingleton{
    public:
     static MySingleton* getInstance(){
     MySingleton* sin= instance.load();
     if ( !sin ){
     std::lock_guard<std::mutex> myLock(myMutex);
     sin= instance.load();
     if( !sin ){
     sin= new MySingleton();
     instance.store(sin);
     }
     } 
     // volatile int dummy{};
     return sin;
     }
    private:
     MySingleton()= default;
     ~MySingleton()= default;
     MySingleton(const MySingleton&)= delete;
     MySingleton& operator=(const MySingleton&)= delete;
    
     static std::atomic<MySingleton*> instance;
     static std::mutex myMutex;
    };
    
    
    std::atomic<MySingleton*> MySingleton::instance;
    std::mutex MySingleton::myMutex;
    
    std::chrono::duration<double> getTime(){
    
     auto begin= std::chrono::system_clock::now();
     for ( size_t i= 0; i <= tenMill; ++i){
     MySingleton::getInstance();
     }
     return std::chrono::system_clock::now() - begin;
     
    };
    
    
    int main(){
    
     auto fut1= std::async(std::launch::async,getTime);
     auto fut2= std::async(std::launch::async,getTime);
     auto fut3= std::async(std::launch::async,getTime);
     auto fut4= std::async(std::launch::async,getTime);
     
     auto total= fut1.get() + fut2.get() + fut3.get() + fut4.get();
     
     std::cout << total.count() << std::endl;
    
    }
    

    今、私は興味があります.

    最適化なし

    最大限の最適化

    しかし、もっとうまくやることができます。追加の最適化の可能性があります。

    アクワイア リリース セマンティック

    シングルトンの読み取り (14 行目) は取得操作であり、書き込み操作は解放操作 (20 行目) です。両方の操作が同じアトミックで行われるため、順次整合性は必要ありません。 C++ 標準では、同じアトミックで取得操作が解放操作と同期することが保証されています。この場合、これらの条件が成り立つため、14 行目と 20 行目で C++ メモリ モデルを弱体化できます。取得と解放のセマンティックで十分です。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    // singletonAcquireRelease.cpp
    
    #include <atomic>
    #include <iostream>
    #include <future>
    #include <mutex>
    #include <thread>
    
    constexpr auto tenMill= 10000000;
    
    class MySingleton{
    public:
     static MySingleton* getInstance(){
     MySingleton* sin= instance.load(std::memory_order_acquire);
     if ( !sin ){
     std::lock_guard<std::mutex> myLock(myMutex);
     sin= instance.load(std::memory_order_relaxed);
     if( !sin ){
     sin= new MySingleton();
     instance.store(sin,std::memory_order_release);
     }
     } 
     // volatile int dummy{};
     return sin;
     }
    private:
     MySingleton()= default;
     ~MySingleton()= default;
     MySingleton(const MySingleton&)= delete;
     MySingleton& operator=(const MySingleton&)= delete;
    
     static std::atomic<MySingleton*> instance;
     static std::mutex myMutex;
    };
    
    
    std::atomic<MySingleton*> MySingleton::instance;
    std::mutex MySingleton::myMutex;
    
    std::chrono::duration<double> getTime(){
    
     auto begin= std::chrono::system_clock::now();
     for ( size_t i= 0; i <= tenMill; ++i){
     MySingleton::getInstance();
     }
     return std::chrono::system_clock::now() - begin;
     
    };
    
    
    int main(){
    
     auto fut1= std::async(std::launch::async,getTime);
     auto fut2= std::async(std::launch::async,getTime);
     auto fut3= std::async(std::launch::async,getTime);
     auto fut4= std::async(std::launch::async,getTime);
     
     auto total= fut1.get() + fut2.get() + fut3.get() + fut4.get();
     
     std::cout << total.count() << std::endl;
    
    }
    

    取得と解放のセマンティックには、順次整合性と同様のパフォーマンスがあります。 x86 では両方のメモリ モデルが非常に似ているため、これは驚くべきことではありません。 ARMv7 または PowerPC アーキテクチャでは、まったく異なる数値が得られます。詳しくは、Jeff Preshing のブログ Preshing on Programming をご覧ください。

    最適化なし

    最大限の最適化

    .

    スレッド セーフなシングルトン パターンのインポート バリアントを忘れた場合は、お知らせください。コードを送ってください。私はそれを測定し、数値を比較に追加します.

    一目ですべての数字

    数字をあまり真剣に考えないでください。各プログラムを 1 回だけ実行しました。実行可能ファイルは、2 コアの Windows PC で 4 コア用に最適化されています。しかし、数字は明確な指標を示しています。 Meyers Singleton は、入手が最も簡単で、最速のものです。特に、ロックベースの実装は最も遅いものです。数値は、使用するプラットフォームに依存しません。

    しかし、数字はそれ以上を示しています。最適化が重要です。このステートメントは、シングルトン パターンの std::lock_guard ベースの実装に完全に当てはまるわけではありません。

    次は?

    私はちょっと確信が持てません。この投稿は、半年前に書いたドイツ語の投稿を翻訳したものです。私のドイツ語の投稿は多くの反響を呼んでいます。今回はどうなるかわかりません。きっと数日手紙。次の投稿は、ベクトルの要素の追加についてです。まず、1 つのスレッドを取り込みます。