C++ 約5分

C++の最適化は設計上の選択がすべて

Share this article
C++の最適化は設計上の選択がすべて

C++プログラミングにおいて、最適化は単にパフォーマンス向上のためにコードを微調整することを超えています。それは本質的に、賢い設計上の選択をすることを中心に展開します。その理由は次のとおりです:

  1. アルゴリズムの選択:アルゴリズムの選択はパフォーマンスに劇的な影響を与える可能性があります。O(n^2)ではなくO(n log n)のソートアルゴリズムを使うことが典型例です。
  2. データ構造:適切なデータ構造の選択(例:高速な検索のために連結リストではなくハッシュテーブルを使う)は、大きな効率改善につながる可能性があります。
  3. メモリ管理:効率的なメモリ使用と割り当て/解放の最小化は、パフォーマンスを向上させられます。メモリプーリングやスマートポインタの適切な使用などの技術が大きな違いを生みます。
  4. 並行性と並列性:複数のスレッドやプロセスを効果的に使用するシステムを設計することで、パフォーマンスを改善できます。C++11はスレッドの標準サポートを導入し、これを支援しています。
  5. 時期尚早な最適化の回避:まずクリーンで保守しやすいコードに焦点を当て、後で重要なセクションを最適化する方が、通常はより効果的です。

Doxygenケーススタディ:設計上の選択によるメモリスラッシングとの戦い

マシン上で実行されているプロセスがシステムの利用可能な量を超えるメモリを割り当てようとすると、カーネルはメモリページをディスクとの間でスワップし始めます。これは、要求者のRAM割り当て要件を満たすために十分な物理メモリを解放するために行われます。

スワッピングの過度な使用は スラッシング と呼ばれ、望ましくありません。主にハードドライブがRAMよりはるかに遅いため、システム全体のパフォーマンスを低下させます。

アプリケーションが大量のデータを使用する必要がある場合、スラッシングにさらされ、アプリケーションが劇的に遅くなる可能性があります。解決策は2つあります:アプリケーションを最適化してメモリをより効率的に使用するか、システムに物理RAMを追加するかです。

Doxygenがメモリ使用を最適化し、スラッシング問題を回避するためにどのソリューションを使っているか見てみましょう。

Doxygenは、注釈付きC++ソースからドキュメントを生成するための事実上の標準ツールですが、C、Objective-C、C#、PHP、Java、Pythonなど、他の多くの人気プログラミング言語もサポートしています。プロジェクトの開発と保守に尽力されているDimitri van Heesch氏に感謝します。

Doxygenはソースファイルを入力として受け取り、それらを解析して必要なデータを抽出し、結果をDirDef、FileDef、NamespaceDef、ClassDef、MemberDefの種類のクラスインスタンスに格納します。これらはすべてDefinitionクラスから継承しています。

doxy7

これらのクラスのインスタンスは、その後ドキュメントの生成に使用されます。最もメモリを消費するデータはメソッドと変数に関する情報で、MemberDefクラスで表現されます。これらのインスタンスのサイズは、処理されるプロジェクトのメソッドと変数の数によって、1GBを超えるまで増大する可能性があります。

一部のプロジェクトでは、これらすべてのインスタンスをメモリに格納するとシステムパフォーマンスに影響し、ドキュメントの生成に何時間もかかる可能性があります。

Doxygenはどうやってメモリを最適化しているのか?

Doxygenはディスクキャッシュベースのソリューションを使用しています。 ディスクキャッシュ の使用は、メモリ使用量を最適化する一般的な方法です。アイデアは、本来メモリに保持する必要があるデータをディスクに保存することです。このキャッシュには多くのスロットがあり、それぞれが特定のデータを含み、キャッシュサイズが特定の値を超えると一部のスロットが解放されます。解放されたデータはディスク上に存在し、再び必要になればメモリに移動されます。

Doxygenの場合、アルゴリズムは非常にシンプルです:

  • 65535スロットのキャッシュを定義します。
  • MemberDefインスタンスを作成する必要がある場合、Doxygenはキャッシュスロットが利用可能かをチェックします。利用可能であれば、インスタンスはメモリに作成されます。そうでなければ、ディスク上のデータファイルに保存され、インデックスファイルが更新されて、そのデータがデータファイルのどこに保存されているかが記録されます。
  • DoxygenがMemberDefインスタンスにアクセスする必要がある場合、キャッシュ内の存在をチェックします。存在しない場合、Doxygenはインデックスファイルを使ってデータがどこに保存されているかを判断し、データファイル内のその位置にシークして、ディスクからロードします。

キャッシュのパフォーマンスは以下に依存します:

  • コンテナ:キュー、配列、リスト、あるいはカスタムコンテナの可能性があります。これらのコンテナの選択がキャッシュのパフォーマンスに影響する可能性があります。
  • キャッシュの最大サイズ。
  • キャッシュからエントリを削除するために使用されるアルゴリズム。キャッシュが最大に達したとき、どのエントリを解放するかを決定する必要があります。例えば:
    - 最初にロードされたスロットを解放する。
    - 最後にロードされたスロットを解放する。
    - 最も使用されていないスロットを解放する。

1- コンテナ

DoxygenはObjCacheクラスを定義しています。これはCacheNodeインスタンスの連結リストで、キャッシュへのインスタンスの追加と削除を担当します。

doxy1

Doxygenがキャッシュをどう宣言しているかは次のとおりです:

Doxygen::symbolCache   =new ObjCache(16+cacheSize);// 16 -> room for 65536 elements, 

2- キャッシュサイズ

Doxygenはキャッシュの最大サイズを設定ファイルから取得します:

int cacheSize =Config_getInt("SYMBOL_CACHE_SIZE");

このパラメータを設定可能にするのは良いアイデアです。大量の物理メモリを持つマシンでキャッシュを増やして、キャッシュのパフォーマンスを向上させられます。ただし、新しいDoxygenリリースでは、このパラメータは設定ファイルから削除され、デフォルト値が使用されています。

3- キャッシュからエントリを削除するアルゴリズム

キャッシュが最大に達したときにキャッシュを解放する、Doxygenソースコードのコードスニペットは次のとおりです:

doxy6

非常によくコメントされているmakeResidentメソッドのコードに記載されているように、キャッシュがいっぱいの場合、最も最近使用されていないアイテムが削除されます。

このメソッドは、ほぼすべてのMemberDefメソッドで呼び出されます。このメンバーがロードされているかどうかを確認するためにMemberDefの状態にアクセスするたびに呼び出され、ロードされていなければロードし、キャッシュから最も最近使用されていないメンバーを削除します。

キャッシュ使用の影響

キャッシュの使用はアプリケーションのパフォーマンスを改善できますが、それは重要な最適化を提供するのでしょうか、それとも追加の複雑さに見合わない単なるマイクロ最適化なのでしょうか?

Clangを製品のC/C++パーサーとして使用する前は、最初のバージョンでDoxygenをパーサーとして使用していました。キャッシュサイズについて多くのテストを行いました。キャッシュを無効にしてこの変更版でいくつかのC++プロジェクトを解析したところ、解析時間が大幅に増加し、5分から25分になることもありました。大規模なプロジェクトでは、数時間かかり、システムパフォーマンスに大きな影響を与える可能性があります。

結論

効果的なC++の最適化は、情報に基づいた設計上の選択に深く根ざしています。適切なアルゴリズム、データ構造、メモリ管理技術を選択し、適切な場所で並行性を活用することで、開発者は大幅なパフォーマンス向上を実現できます。この設計への戦略的アプローチこそが、C++における最適化を真に推進するものです。

Share this article