導入:常識を疑うパフォーマンスの最適化
C++エンジニアの皆さん、普段「大きなオブジェクトはコピーを避けるためにconst参照渡しにする」という習慣が染み付いていませんか?もちろんそれは正しいですが、現代のC++開発では「小さなオブジェクトは値渡しの方が高速」というケースが多々あります。なぜ「参照(ポインタ)」を使わない方が効率的なのか、その理由をABI(Application Binary Interface)の仕組みから紐解いていきましょう。
基礎知識:ABIとレジスタ渡しの仕組み
C++の関数呼び出しには、OSやプロセッサごとに決められた「呼び出し規約(ABI)」が存在します。例えば、Linux(System V AMD64 ABI)では、関数に引数を渡す際、可能な限りスタックを使わず、CPUの「レジスタ」を利用します。
レジスタはメモリ(RAM)と比較して圧倒的に高速です。もし引数がレジスタに収まるサイズであれば、CPUはメモリへアクセスすることなく計算を開始できます。しかし、参照渡し(const T&)を選んでしまうと、コンパイラは強制的に「メモリ上のアドレス」を渡すことになり、関数内部でそのアドレスを辿る(間接参照)という余分なステップが発生します。
実装・解決策:軽量オブジェクトは値渡しで
`std::string_view` や `std::span` といった軽量なビュー・オブジェクトは、内部的に「ポインタ」と「サイズ(長さ)」の2つを持つ構造体です。合計16バイト(64bit環境)で構成されるため、ABIの規定によりレジスタにぴったり収まります。
これらを `const std::string_view&` で受けると、呼び出し側で一度メモリに値を置き、そのアドレスをレジスタにロードしてから関数を呼ぶという、無駄な処理が加わります。値渡し(`std::string_view`)にすれば、レジスタに直接値をコピーするだけで済み、メモリロードのコストを排除できます。
サンプルプログラム
以下のコードで、レジスタ渡しを意識した設計の例を示します。
include
include
// 良い例: 値渡し
// 16バイト以下のTrivially Copyableな型なのでレジスタに直接乗る
void process_efficiently(std::string_view sv) {
// メモリへの間接参照なしでsv.data()等にアクセス可能
std::cout << "処理中: " << sv << std::endl;
}
// 悪い例: 参照渡し
// スタック上のアドレスを指すポインタが渡されるため、
// 関数内で値を取り出す際にメモリ読み込み(L1キャッシュアクセス)が発生する
void process_inefficiently(const std::string_view& sv) {
std::cout << "処理中: " << sv << std::endl;
}
int main() {
std::string_view text = "Hello, ABI!";
// レジスタ渡しにより高速に実行される
process_efficiently(text);
return 0;
}
応用・注意点:現場で陥りやすい罠
この最適化を行う際の注意点は、「Trivially Copyable(コピーが単純)」であるかどうかです。`std::string` のように内部でヒープ領域を確保するようなクラスは、値渡しをするとコピーコンストラクタが走って深層コピーが発生し、逆に非常に低速になります。
今回のTipsが適用できるのは、あくまで「ポインタとサイズのみを持つような軽量な構造体」や「基本データ型(int, double等)」に限られます。現場でクラスを設計する際は、そのオブジェクトがメモリを所有しているのか、それとも単なるデータの「窓(ビュー)」なのかを意識することで、パフォーマンスを一段階引き上げることが可能です。

コメント