अंतिम अद्यतन:2026-10-12
C++ restrict और कंपाइलर अनुकूलन¶
ClickHouse के एक PR (ClickHouse#19946) की review करते समय मुझे __restrict का व्यावहारिक उपयोग दिखा और मैंने तुरंत इस पर थोड़ा अध्ययन कर लिया: एक keyword कंपाइलर को कितना अनुकूलन-अवसर दे सकता है? पहले निष्कर्ष: "कंपाइलर यह सिद्ध न कर पाए कि दो pointers alias नहीं करते" ऐसे hot loops में restrict स्पष्ट रूप से दिखने वाला सुधार देता है।
1. पृष्ठभूमि: pointer aliasing कोड को धीमा क्यों करता है¶
कंपाइलर loop optimization करते समय सबसे ज़्यादा डरता है कि दो pointers एक ही memory की ओर इशारा करें (aliasing)। यह क्लासिक परिदृश्य देखें:
अगर out और a alias करते हों, तो out[i] में लिखना a[i] को बदल सकता है; इसलिए कंपाइलर reads/writes को पुनर्व्यवस्थित करने का, vector instructions इस्तेमाल करने का जोखिम नहीं उठाता और ईमानदारी से एक-एक instruction चलाता है। वहीं restrict प्रोग्रामर का कंपाइलर से वादा है: इस pointer से access होने वाली memory किसी दूसरे pointer से alias नहीं करेगी। वादा मिलते ही कंपाइलर खुलकर चल पड़ता है।
2. वास्तविक मापन: aggregate गणना का hot path¶
नीचे दिया benchmark डेटाबेस एग्रीगेशन गणना के hot path का अनुकरण करता है (ClickHouse PR का सरलीकृत संस्करण); दोनों संस्करणों में एकमात्र अंतर __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://github.com/ClickHouse/ClickHouse/pull/19946
3. परिणाम और सारांश¶
(1) __restrict वाला संस्करण (BM_loop_with_optimize) स्पष्ट रूप से तेज़ है: numerator/denominator दोनों state pointers annotate होने पर कंपाइलर उन्हें registers में रखता है और loop पूरी तरह vectorize हो जाता है; बिना annotation वाला संस्करण हर बार memory से फिर load करता है;
(2) restrict फिलहाल मानक C++ keyword नहीं है; प्रचलित तरीका compiler extensions हैं: GCC/Clang __restrict (या __restrict__) इस्तेमाल करते हैं, MSVC __restrict, और C++26 में standardization पर चर्चा चल रही है (std::indirect/no_alias प्रस्ताव);
(3) चेतावनी: restrict बिना बीमा का वादा है—— aliasing हुई तो undefined behavior। केवल उन hot paths पर इस्तेमाल करें जहाँ आप no-aliasing सिद्ध कर सकते हों (जैसे allocator से लौटाए गए स्वतंत्र state blocks)।
एक वाक्य में सार: hot loop में कई pointers से पढ़ना-लिखना हो और सचमुच aliasing न हो, तो __restrict अत्यंत लाभदायक अनुकूलन साधन है। परिशिष्ट कोड ऊपर के benchmark में है; रुचि हो तो quick-bench पर खुद चलाकर देखें।
संबंधित विषय¶
- C++ प्योर वर्चुअल फ़ंक्शन और एब्स्ट्रैक्ट क्लास | C++那些事
- C++ assert | C++那些事
- C++ bit-field | C++那些事
- C भाषा से C++ पॉलीमॉर्फ़िज़्म | C++那些事
- C++ const constant और qualifier | C++那些事
- C++ const परिवार | C++那些事
- C++ dereference ऑपरेटर * | C++那些事
- C++ इनिशियलाइज़ेशन और असाइनमेंट | C++那些事
- C++ multiple return values लिखने के तरीके | C++那些事
- C++ mutable कीवर्ड | C++那些事