コンテンツにスキップ

最終更新:2026-10-12

C++ restrict とコンパイル最適化

ClickHouse の PR(ClickHouse#19946)をレビューしていたとき、__restrict の実戦的な使い方を見かけたので、ついでに調べてみました:キーワードひとつでコンパイラにどれだけの最適化の余地を与えられるのか? 結論から言うと:「コンパイラが 2 つのポインタがエイリアスしないことを証明できない」ホットループでは、restrict は目に見える向上をもたらします。

1.背景:ポインタエイリアシング(aliasing)がコードを遅くする理由

コンパイラがループ最適化を行うとき、一番怖いのが 2 つのポインタが同じメモリを指すこと(エイリアシング)です。次の古典的なシナリオを見てください:

void add(int* a, int* b, int* out, int n) {
  for (int i = 0; i < n; ++i) {
    out[i] = a[i] + b[i];
  }
}

out と a がエイリアスしていると、out[i] への書き込みが a[i] を変えてしまう可能性があるため、コンパイラは読み書きの並べ替えもベクトル命令の使用もできず、おとなしく 1 本ずつ実行するしかありません。一方 restrict は、プログラマからコンパイラへの**約束**です:このポインタ経由でアクセスするメモリは、他のポインタとエイリアスしません。約束すれば、コンパイラは思い切って動けます。

2.実測:aggregate 計算のホットスポット

次の benchmark はデータベース集計計算のホットパスを模したもの(ClickHouse PR の簡略版)で、2 つのバージョンの唯一の違いは __restrict です:

#include <vector>
#include <memory>
#include <benchmark/benchmark.h>

#define LEN 1<<20
using AggregateDataPtr = char *;

struct AvgState {
  uint64_t numerator{0};
  uint64_t denominator{0};

  double divide() { return numerator / denominator; }
};

class Column {
public:
  std::vector<uint64_t> & data() {return data_; }

private:
  std::vector<uint64_t> data_;
};

template <class Derived>
class IAggregate {
public:
  void addBatch(size_t batch_size, AggregateDataPtr state, Column *args) __attribute__((noinline)) {
      for (size_t i = 0; i < batch_size; ++i) {
          static_cast<const Derived *>(this)->add(state, args, i);
      }
  }

  void addBatchWithoutOpt(size_t batch_size, AggregateDataPtr state, Column *args) __attribute__((noinline)) {
      for (size_t i = 0; i < batch_size; ++i) {
          static_cast<const Derived *>(this)->addWithoutOpt(state, args, i);
      }
  }
};

class AvgAggregator final : public IAggregate<AvgAggregator> {
public:
  static AvgState& data(AggregateDataPtr __restrict place) { return *reinterpret_cast<AvgState *>(place); }
  static AvgState& data_without_opt(AggregateDataPtr place) { return *reinterpret_cast<AvgState *>(place); }

  void add(AggregateDataPtr __restrict state, Column * args, uint64_t row_num) const {
    data(state).numerator += args[0].data()[row_num];
    ++(data(state).denominator);
  }

  void addWithoutOpt(AggregateDataPtr state, Column * args, uint64_t row_num) const {
    data_without_opt(state).numerator += args[0].data()[row_num];
    ++(data_without_opt(state).denominator);
  }
};

static void BM_loop_with_optimize(benchmark::State& state) {
    AvgAggregator avgAggregator;
    Column column;
    for (size_t i = 0 ; i < LEN; i++) {
        column.data().push_back(i);
    }
    Column arg0[] = {column};
    // Code inside this loop is measured repeatedly
    for(auto _ : state) {
        auto avgState = std::make_unique<AvgState>();
        avgAggregator.addBatch(LEN, reinterpret_cast<AggregateDataPtr>(avgState.get()), arg0);
    }
}

static void BM_loop_without_optimize(benchmark::State& state) {
    AvgAggregator avgAggregator;
    Column column;
    for (size_t i = 0 ; i < LEN; i++) {
        column.data().push_back(i);
    }
    Column arg0[] = {column};
    // Code inside this loop is measured repeatedly
    for(auto _ : state) {
        auto avgState = std::make_unique<AvgState>();
        avgAggregator.addBatchWithoutOpt(LEN, reinterpret_cast<AggregateDataPtr>(avgState.get()), arg0);
    }
}

BENCHMARK(BM_loop_without_optimize);
BENCHMARK(BM_loop_with_optimize);

https://quick-bench.com/q/ccjuWr_JcLozAfy3Gxf3uVu76Yk

https://github.com/ClickHouse/ClickHouse/pull/19946

3.結果とまとめ

(1)__restrict 付きのバージョン(BM_loop_with_optimize)は明らかに高速です:numerator/denominator という 2 つの状態ポインタに注釈を付けると、コンパイラはそれらをレジスタに置き、ループは完全にベクトル化されます;注釈なしのバージョンは毎回メモリから再ロードします;

(2)restrict は現時点で標準の C++ キーワードではなく、主流の書き方はコンパイラ拡張です:GCC/Clang は __restrict(または __restrict__)、MSVC は __restrict を使い、C++26 では標準化が議論されています(std::indirect/no_alias 提案);

(3)警告:restrict は**保険のない約束**です。エイリアスすれば未定義動作です。ホットパスであり、かつエイリアスなしを証明できる場所(たとえばアロケータが返す独立した状態ブロック)だけで使ってください。

一言でまとめると:ホットループで複数ポインタの読み書きがあり、本当にエイリアスしないなら、__restrict は費用対効果の非常に高い最適化手段です。付属コードは上の benchmark にあるので、興味があれば quick-bench で自分で走らせてみてください。

関連トピック

コメント