Optimized C++

―最適化、高速化のためのプログラミングテクニック

[cover photo]
TOPICS
Programming , C/C++
発行年月日
PRINT LENGTH
368
ISBN
978-4-87311-792-8
原書
Optimized C++
FORMAT
Print PDF
Ebook
4,400円
Ebookを購入する
Print
4,400円

C++プログラムの性能には、ハードウェア、コンパイラ、データ構造、アルゴリズム、ライブラリといったさまざまな要因が関係します。本書は性能に影響する要因の特性をしっかり理解し、正しく測定することによって性能上の問題を引き起こしている「ホットスポット」を特定し、どのような最適化が可能であり、採用すべきなのかを詳しく解説します。従来の文や式の最適化、コンパイラオプションだけでなく、性能チューニングの原則と、文字列、アルゴリズム、動的変数割り当て、カスタムライブラリ、探索と整列、データ構造、入出力、並列処理、メモリ管理といったあらゆる角度からの最適化テクニックを、「コード中毒」の著者が実際に直面したエピソードを交え紹介します。より高速なプログラムを必要とするプログラマに不可欠な内容です。C++11/C++14対応。

関連ファイル

正誤表

ここで紹介する正誤表には、書籍発行後に気づいた誤植や更新された情報を掲載しています。以下のリストに記載の年月は、正誤表を作成し、増刷書籍を印刷した月です。お手持ちの書籍では、すでに修正が施されている場合がありますので、書籍最終ページの奥付でお手持ちの書籍の刷版、刷り年月日をご確認の上、ご利用ください。

2刷正誤表


※2018年7月更新。2刷で修正済みです。

■p.29 1行目数式
【誤】ST = 1 / (1-0.1) + P/SP
【正】ST = 1 / (1-P) + P/SP

■p.167 下から2行目
【誤】7.4.3.1 例外指定を使わない
【正】7.4.3.1 非推奨の例外指定を使わない

■p.168 上から4,5段落目差し替え
【誤】
伝統的な例外指定は、C++11で非推奨となった。
C++11は、noexceptと呼ばれる新たな例外指定を導入した。関数をnoexceptと宣言すると、コンパイラには関数が例外を投げることはないと知らせる。もし関数が例外を投げると、throw()の規定にあるようにterminate()が呼ばれる。相違点は、コンパイラにはムーブコンストラクタとムーブ代入文とがnoexceptと宣言されて、ムーブセマンティクス(「6.6 ムーブセマンティクスの実装」参照)が実装される必要があることだ。関数のnoexcept指定は、ある種のオブジェクトにはムーブセマンティクスが強い例外安全性保証より重要だと宣言する役割を果たす。これはとてもわかりにくい。
【正】
伝統的な例外指定は、C++11で非推奨となった。だから、使ってはならない。
C++11は、noexceptと呼ばれる新たな例外指定を導入した。関数をnoexceptと宣言すると、コンパイラには関数が例外を投げることはないと知らせる。もし関数が例外を投げようとすると、プログラムはterminate()を呼んで直ちに停止する。C++では、クラスがstd::vectorのようなコンテナで使われ、強い例外安全性を保証する場合には、ムーブコンストラクタとムーブ代入演算子関数をnoexceptと宣言するには、ムーブセマンティクス(「6.6 ムーブセマンティクスの実装」を参照)を実装する必要がある。
ムーブセマンティクスを取得してnoexceptを使い、プログラムが突然何の条件もなく停止する危険性を受け入れるか、それともnoexceptは使わず、性能低下を受け入れるかどちらにするかは開発者にとっては難しいところだ。標準の作成者すら決定できなかった。ムーブコンストラクタとムーブ代入演算子とは例外を投げるべきではないのだが、noexceptである必要はない。

目次

日本語版へ寄せて
訳者まえがき
まえがき

1章 最適化とは
    1.1 最適化はソフトウェア開発の一部
    1.2 最適化は効果的
    1.3 最適化しても大丈夫
    1.4 こちらにナノ秒、あちらにナノ秒
    1.5 最適化C++コード戦略のまとめ
        1.5.1 より良いコンパイラを使う、コンパイラをよりうまく使う
        1.5.2 より良いアルゴリズムを使う
        1.5.3 より良いライブラリを使う
        1.5.4 メモリ割り当てとコピーを減らす
        1.5.5 計算を取り除く
        1.5.6 より良いデータ構造を使う
        1.5.7 並行性を高める
        1.5.8 メモリ管理を最適化する
    1.6 まとめ

2章 最適化に影響するマシンの振る舞い
    2.1 C++の嘘と真実
    2.2 コンピュータについての真実
        2.2.1 メモリは遅い
        2.2.2 メモリはバイト単位でアクセスされているのではない
        2.2.3 メモリアクセスによっては他より遅いことがある
        2.2.4 メモリ語にはビッグエンディアンとリトルエンディアンがある
        2.2.5 メモリ容量は有限
        2.2.6 命令実行は遅い
        2.2.7 コンピュータは決定が下手だ
        2.2.8 プログラム実行に複数のストリームがある
        2.2.9 オペレーティングシステム呼び出しは高価
    2.3 C++も嘘をつく
        2.3.1 文がすべて高価なわけではない
        2.3.2 文は順番に実行されない
    2.4 まとめ

3章 性能を測定する
    3.1 最適化マインドセット
        3.1.1 性能は計測されねばならない
        3.1.2 最適化技術者は大物狙いの狩人だ
        3.1.3 90/10ルール
        3.1.4 アムダールの法則
    3.2 実験を行う
        3.2.1 実験ノートをつける
        3.2.2 ベースライン性能を測定し目標を立てる
        3.2.3 測定できるものだけが改善できる
    3.3 プログラム実行のプロファイルを取る
    3.4 長時間実行コードを計測する
        3.4.1 時間計測の「ちょっとした学習」
        3.4.2 コンピュータで時間を測る
        3.4.3 測定する際の障害を乗り越える
        3.4.4 Stopwatchクラスを作る
        3.4.5 テストハーネスでホットな関数の時間を測る
    3.5 ホットなコードを探し出すためにコードのコストを見積もる
        3.5.1 C++文それぞれのコストを見積もる
        3.5.2 ループのコストを見積もる
    3.6 ホットスポットを探し出す他の方法
    3.7 まとめ

4章 文字列使用を最適化する:事例研究
    4.1 なぜ文字列が問題か
        4.1.1 文字列は動的に割り当てられる
        4.1.2 文字列は値だ
        4.1.3 文字列はコピーが多い
    4.2 文字列最適化の最初の試み
        4.2.1 一時ストレージをなくすために変更文字列演算を使う
        4.2.2 ストレージを確保することで再割り当てを減らす
        4.2.3 文字列引数のコピーをなくす
        4.2.4 イテレータを使ってポインタ参照外しをなくす
        4.2.5 文字列値の戻り値のコピーをなくす
        4.2.6 文字列ではなく文字配列を使う
        4.2.7 最初の最適化努力のまとめ
    4.3 文字列最適化の第2の試み
        4.3.1 より良いアルゴリズムを使う
        4.3.2 より良いコンパイラを使う
        4.3.3 より良い文字列ライブラリを使う
        4.3.4 より良いアロケータを使う
    4.4 文字列変換をなくす
        4.4.1 C文字列からstd::stringへの変換
        4.4.2 文字符号化間での変換
    4.5 まとめ

5章 アルゴリズムを最適化する
    5.1 アルゴリズムの時間コスト
        5.1.1 最良時、平均時、および最悪時コスト
        5.1.2 ならし時間コスト
        5.1.3 他のコスト
    5.2 探索と整列を最適化するツールキット
    5.3 効率的探索アルゴリズム
        5.3.1 探索アルゴリズムの時間コスト
        5.3.2 すべての探索はn が小さいなら等しい
    5.4 効率的整列アルゴリズム
        5.4.1 整列アルゴリズムの時間コスト
        5.4.2 最悪時性能のソートを置き換える
        5.4.3 入力データの既知の特性を活用する
    5.5 最適化のパターン
        5.5.1 事前計算
        5.5.2 遅延計算
        5.5.3 バッチ処理
        5.5.4 キャッシュ
        5.5.5 特殊化
        5.5.6 大量データ処理
        5.5.7 ヒント
        5.5.8 期待パスを最適化する
        5.5.9 ハッシング
        5.5.10 ダブルチェック
    5.6 まとめ

6章 動的変数割り当てを最適化する
    6.1 C++ 変数の復習
        6.1.1 変数の記憶域期間
        6.1.2 変数の所有権
        6.1.3 値オブジェクトとエンティティオブジェクト
    6.2 C++動的変数APIの復習
        6.2.1 スマートポインタは動的変数の所有権を自動化する
        6.2.2 動的変数には実行時のコストがかかる
    6.3 動的変数の使用を減らす
        6.3.1 クラスインスタンスを静的に作る
        6.3.2 静的データ構造を使う
        6.3.3 new の代わりにstd::make_sharedを使う
        6.3.4 所有権を不必要に共有しない
        6.3.5 「マスターポインタ」を使って動的変数を所有する
    6.4 動的変数の再割り当てを減らす
        6.4.1 動的変数を前もって割り当てて再割り当てを防ぐ
        6.4.2 動的変数をループの外で作る
    6.5 不必要なコピーをなくす
        6.5.1 望まないコピーをクラス定義で無効にする
        6.5.2 関数呼び出しでのコピーをなくす
        6.5.3 関数から戻るときのコピーをなくす
        6.5.4 コピーフリーライブラリ
        6.5.5 「コピーオンライト」イディオムを実装する
        6.5.6 データ構造をスライスする
    6.6 ムーブセマンティクスの実装
        6.6.1 非標準的コピーセマンティクス:痛みを伴うハッキング
        6.6.2 std::swap():貧乏人のムーブセマンティクス
        6.6.3 エンティティの共有所有権
        6.6.4 ムーブセマンティクスの移動部分
        6.6.5 ムーブセマンティクスを用いてコードを更新する
        6.6.6 ムーブセマンティクスの難しいところ
    6.7 データ構造をフラットにする
    6.8 まとめ

7章 ホットな文を最適化する
    7.1 ループからコードを取り除く
        7.1.1 ループの最後の値をキャッシュする
        7.1.2 より効率的なループ文を使う
        7.1.3 カウントアップの代わりにカウントダウンする
        7.1.4 不変なコードをループから取り除く
        7.1.5 不必要な関数呼び出しをループから取り除く
        7.1.6 ループから隠された関数呼び出しを取り除く
        7.1.7 高価で値がゆっくり変わる呼び出しをループから取り除く
        7.1.8 呼び出しのオーバーヘッドを減らすためにループを関数の内側に押し込める
        7.1.9 処理の頻度を減らす
        7.1.10 その他にできること
    7.2 関数のコードを取り除く
        7.2.1 関数呼び出しのコスト
        7.2.2 短い関数はインラインと宣言する
        7.2.3 最初に使う前に関数が定義されている状態にする
        7.2.4 使っていないポリモルフィズムをなくす
        7.2.5 使っていないインタフェースを取り除く
        7.2.6 テンプレートでコンパイル時に実装を選ぶ
        7.2.7 PIMPLイディオムの使用を止める
        7.2.8 DLL呼び出しをなくす
        7.2.9 メンバ関数の代わりに静的メンバ関数を使う
        7.2.10 仮想デストラクタを基底クラスに移す
    7.3 式を最適化する
        7.3.1 式を単純化する
        7.3.2 定数をまとめる
        7.3.3 より安価な演算子を使う
        7.3.4 浮動小数点算術よりも整数算術を使う
        7.3.5 double はfloat より速い場合がある
        7.3.6 反復計算を閉形式で置き換える
    7.4 制御フローイディオムを最適化する
        7.4.1 if-else if-elseよりもswitchを使う
        7.4.2 switchやifの代わりに仮想関数を使う
        7.4.3 コストなしの例外処理を使う
    7.5 まとめ

8章 優れたライブラリを使う
    8.1 標準ライブラリ利用を最適化する
        8.1.1 C++標準ライブラリの哲学
        8.1.2 C++標準ライブラリを使うときの問題
    8.2 既存ライブラリを最適化する
        8.2.1 変更をできる限り減らす
        8.2.2 機能を変更するよりは関数を追加する
    8.3 最適化したライブラリの設計
        8.3.1 急いでコード化し、ゆっくり後悔せよ
        8.3.2 節約はライブラリ設計での美徳
        8.3.3 メモリ割り当て決定をライブラリの外で行う
        8.3.4 迷ったらライブラリのコードは速度優先にする
        8.3.5 関数はフレームワークより最適化しやすい
        8.3.6 継承階層を平坦にする
        8.3.7 呼び出し連鎖を平坦にする
        8.3.8 設計の層を平坦にする
        8.3.9 動的参照を止める
        8.3.10 「全能関数」に注意
    8.4 まとめ

9章 探索と整列を最適化する
    9.1 std::mapとstd::stringを使ったキー・値テーブル
    9.2 探索性能を改善するツールキット
        9.2.1 ベースライン測定を行う
        9.2.2 最適化する作業を仕分ける
        9.2.3 最適化する作業を分解する
        9.2.4 アルゴリズムとデータ構造を変更または置き換える
        9.2.5 カスタム抽象化に最適化プロセスを使う
    9.3 std::mapを使って探索を最適化する
        9.3.1 std::mapで固定サイズ文字配列キーを使う
        9.3.2 std::mapでCスタイル文字列キーを使う
        9.3.3 キーが値の中にあればマップの従兄弟のstd::setを使う
    9.4 <algorithm>ヘッダを使って探索を最適化する
        9.4.1 シーケンスコンテナでキー・値テーブルを探索する
        9.4.2 std::find():名前は明らか、O(n) 時間コスト
        9.4.3 std::binary_search()は値を返さない
        9.4.4 std::equal_range()を使った二分探索
        9.4.5 std::lower_bound()を使った二分探索
        9.4.6 自前コードで二分探索
        9.4.7 strcmp()を使った自前コードで二分探索
    9.5 キー・値ハッシュテーブルで探索を最適化する
        9.5.1 std::unordered_mapでハッシュする
        9.5.2 固定文字配列キーでハッシュする
        9.5.3 ナル終端文字列キーでハッシュする
        9.5.4 カスタムハッシュテーブルでハッシュする
    9.6 ステパノフの抽象化ペナルティ
    9.7 C++標準ライブラリを使って整列を最適化する
    9.8 まとめ

10章 データ構造を最適化する
    10.1 標準ライブラリコンテナをよく知る
        10.1.1 シーケンスコンテナ
        10.1.2 連想コンテナ
        10.1.3 標準ライブラリコンテナで実験する
    10.2 std::vectorとstd::string
        10.2.1 再割り当ての性能結果
        10.2.2 std::vectorでの挿入削除
        10.2.3 std::vectorでのイテレーション
        10.2.4 std::vectorを整列する
        10.2.5 std::vectorで探索する
    10.3 std::deque
        10.3.1 std::dequeでの挿入削除
        10.3.2 std::dequeでのイテレーション
        10.3.3 std::dequeを整列する
        10.3.4 std::dequeで探索する
    10.4 std::list
        10.4.1 std::listでの挿入削除
        10.4.2 std::listでのイテレーション
        10.4.3 std::listを整列する
        10.4.4 std::listで探索する
    10.5 std::forward_list
        10.5.1 std::forward_listでの挿入削除
        10.5.2 std::forward_listでのイテレーション
        10.5.3 std::forward_listを整列する
        10.5.4 std::forward_listで探索する
    10.6 std::mapとstd::multimap
        10.6.1 std::mapでの挿入削除
        10.6.2 std::mapでイテレーション
        10.6.3 std::mapを整列する
        10.6.4 std::mapで探索する
    10.7 std::setとstd::multiset
    10.8 std::unordered_mapとstd::unordered_multimap
        10.8.1 std::unordered_mapでの挿入削除
        10.8.2 std::unordered_mapでのイテレーション
        10.8.3 std::unordered_mapで探索する
    10.9 他のデータ構造
    10.10 まとめ

11章 I/O を最適化する
    11.1 ファイル読み込みのためのレシピ
        11.1.1 節約的関数シグネチャを作る
        11.1.2 呼び出し連鎖を縮める
        11.1.3 再割り当てを減らす
        11.1.4 より大きな単位で―より大きな入力バッファを使う
        11.1.5 より大きな単位にする―一度に1行読み込む
        11.1.6 呼び出し連鎖を再度縮める
        11.1.7 役に立たない事柄
    11.2 ファイル書き出し
    11.3 std::cinから読み込みstd::coutに書き出す
    11.4 まとめ

12章 並行性を最適化する
    12.1 C++ 並行機能の復習
        12.1.1 さまざまな並行処理機能の紹介
        12.1.2 並行プログラムのインタリーブ実行
        12.1.3 逐次一貫性
        12.1.4 競合
        12.1.5 同期
        12.1.6 アトミック性
    12.2 C++並行処理機能の復習
        12.2.1 スレッド
        12.2.2 promiseとfuture
        12.2.3 非同期タスク
        12.2.4 mutex
        12.2.5 ロック
        12.2.6 条件変数
        12.2.7 共有変数のアトミック演算
        12.2.8 今後予定されている将来のC++並行処理機能
    12.3 C++スレッドプログラムの最適化
        12.3.1 std::threadよりstd::asyncを使う
        12.3.2 コアと同じ個数の実行可能スレッドを作る
        12.3.3 タスクキューとスレッドプールを実装する
        12.3.4 別のスレッドでI/Oを実行する
        12.3.5 同期なしのプログラム
        12.3.6 起動とシャットダウンからコードを除く
    12.4 同期をより効率的に行う
        12.4.1 クリティカルセクションの範囲を減らす
        12.4.2 並行スレッドの個数を限定する
        12.4.3 途方もない大群を避ける
        12.4.4 ロック護送列を避ける
        12.4.5 競合を減らす
        12.4.6 単一コアシステムでビジーウェイトしない
        12.4.7 永遠に待ってはならない
        12.4.8 自分でmutexを作ると非効率
        12.4.9 生産者出力キューの長さを制限する
    12.5 並行処理ライブラリ
    12.6 まとめ

13章 メモリ管理を最適化する
    13.1 C++メモリ管理APIの復習
        13.1.1 動的変数のライフサイクル
        13.1.2 メモリ管理関数がメモリを割り当てて解放する
        13.1.3 new式で動的変数を作る
        13.1.4 delete式で動的変数を削除する
        13.1.5 明示的デストラクタ呼び出しは動的変数を破壊する
    13.2 高性能メモリマネージャ
    13.3 クラス専用メモリマネージャを提供する
        13.3.1 固定サイズブロックメモリマネージャ
        13.3.2 ブロックアリーナ
        13.3.3 クラス専用operator new()を追加する
        13.3.4 固定ブロックメモリマネージャの性能
        13.3.5 さまざまな固定ブロックメモリマネージャ
        13.3.6 非スレッドセーフメモリマネージャは効率的だ
    13.4 カスタムな標準ライブラリアロケータを提供する
        13.4.1 極小C++11アロケータ
        13.4.2 C++98アロケータの追加定義
        13.4.3 固定ブロックアロケータ
        13.4.4 文字列の固定ブロックアロケータ
    13.5 まとめ
索引