← Kursa Dön
📄 Text · 15 min

Benchmarking: Google Benchmark

Bir doktor düşün. Hasta gelip "biraz yorgunum" dediğinde doktor rastgele bir ilaç yazmaz — kan tahlili ister, değerleri inceler, sonra teşhis koyar. Yazılım performansında da aynı disiplin gerekir. "Bu kod yavaş gibi" deyip rastgele optimizasyon yapmak, tahlilsiz ilaç yazmak gibidir: belki işe yarar, belki daha kötü eder, belki tamamen alakasız bir şeyi "tedavi" edersin.

Benchmarking, kodun performansını ölçülebilir, tekrarlanabilir, karşılaştırılabilir şekilde değerlendirmektir. Donald Knuth'un meşhur sözü var: *"Premature optimization is the root of all evil."* Ama ölçüm yapmadan hangi optimizasyonun "premature" hangisinin gerekli olduğunu bilemezsin. Bu derste önce std::chrono ile elle zamanlama yapacağız ve neden yetersiz olduğunu göreceğiz, sonra Google Benchmark ile profesyonel benchmark yazımına geçeceğiz.


Neden Benchmark Gerekli?

Sezgi Yanıltır

Programcıların performans sezgisi çoğu zaman yanlıştır. Modern CPU'lar inanılmaz karmaşıktır — branch prediction, out-of-order execution, cache hierarchy, SIMD otomatik vektörizasyon. Bu karmaşıklık yüzünden hangi kodun daha hızlı çalışacağını tahmin etmek neredeyse imkansızdır.

İki döngü mü yoksa tek döngü mü daha hızlı? std::map mi std::unordered_map mi? std::string birleştirmede += mi stringstream mi? Bu soruların cevabı "duruma bağlı" — veri boyutuna, cache davranışına, derleyici optimizasyonuna bağlı. Tek doğru cevap: ölçmek.

Premature vs Measured Optimization

İki yaklaşım arasındaki fark kritik:

YaklaşımTanımSonuç
Premature optimizationÖlçmeden "burayı hızlandırayım"Yanlış yere zaman harcama, kod karmaşıklığı
Measured optimizationÖlç → darboğazı bul → orayı optimize etDoğru yere odaklanma, gerçek iyileştirme

Doğru yaklaşım: Önce çalışan, temiz kod yaz. Performans problemi varsa benchmark ile ölç, darboğazı bul, sadece orayı optimize et. Optimizasyon her zaman bir trade-off getirir — kod karmaşıklığı, bakım zorluğu, okunabilirlik kaybı. Bu bedeli ancak ölçüm haklı kılar.


std::chrono ile Manuel Zamanlama

C++ standart kütüphanesinde std::chrono ile zaman ölçebilirsin. İlk yaklaşım olarak makul görünür ama ciddi sorunları vardır. Aşağıdaki örnek hem basit hem de "biraz daha iyi" bir zamanlama gösterir:

#include <iostream>
#include <vector>
#include <chrono>
#include <algorithm>
#include <numeric>
#include <cmath>

// Yaklaşım 1: Tek ölçüm — kırılgan, güvenilmez
void naive_benchmark() {
    const int N = 1000000;
    std::vector<int> data(N);
    std::iota(data.begin(), data.end(), 0);

    auto start = std::chrono::high_resolution_clock::now();
    std::sort(data.begin(), data.end(), std::greater<int>());
    auto end = std::chrono::high_resolution_clock::now();

    auto us = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
    std::cout << "Sort suresi: " << us.count() << " us\n";
    // Her çalıştırmada farklı değer — OS scheduler, background process,
    // termal throttling... tek ölçüm hiçbir şey söylemez.
}

// Yaklaşım 2: Çoklu ölçüm + istatistik — daha iyi ama hâlâ sorunlu
void better_benchmark() {
    const int N = 1000000;
    const int RUNS = 100;
    std::vector<double> times;
    times.reserve(RUNS);

    for (int run = 0; run < RUNS; ++run) {
        std::vector<int> data(N);
        std::iota(data.begin(), data.end(), 0);

        auto start = std::chrono::high_resolution_clock::now();
        std::sort(data.begin(), data.end(), std::greater<int>());
        auto end = std::chrono::high_resolution_clock::now();

        auto ns = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start);
        times.push_back(static_cast<double>(ns.count()));
    }

    double mean = std::accumulate(times.begin(), times.end(), 0.0) / RUNS;
    double variance = 0.0;
    for (auto t : times) variance += (t - mean) * (t - mean);
    double stddev = std::sqrt(variance / RUNS);

    std::sort(times.begin(), times.end());
    double median = times[RUNS / 2];

    std::cout << "Ortalama: " << mean / 1000 << " us\n";
    std::cout << "Median:   " << median / 1000 << " us\n";
    std::cout << "StdDev:   " << stddev / 1000 << " us\n";
}

int main() {
    std::cout << "=== Naive ===\n";
    naive_benchmark();
    std::cout << "\n=== Better ===\n";
    better_benchmark();
    return 0;
}

Manuel Zamanlamanın Sorunları

İkinci yaklaşım daha iyi ama hâlâ birçok sorunu var:

  1. Dead code elimination: Derleyici, sonucu kullanılmayan hesaplamayı tamamen kaldırabilir. Ölçtüğünü sandığın kodun aslında hiç çalışmadığını fark etmezsin.

  2. Cache warming: İlk çalıştırma soğuk cache ile yavaş, sonrakiler sıcak cache ile hızlı. Hangisi "gerçek" performans?

  3. OS scheduler gürültüsü: Arka plandaki process'ler CPU çalabilir. Farklı çalıştırmalarda farklı sonuçlar alırsın.

  4. İstatistik yetersizliği: Kaç kez tekrarlamalı? Outlier'ları nasıl eleyeceksin? Confidence interval nedir? Hepsini sen hesaplamalısın.

  5. Boilerplate yükü: Her yeni test için aynı zamanlama, istatistik ve raporlama kodunu tekrar yazmak gerekir.

Bu sorunların hepsini çözen profesyonel bir araç var: Google Benchmark.


Google Benchmark: Kurulum

CMake ile FetchContent

Google Benchmark'ı projeye eklemenin en temiz yolu FetchContent:

cmake_minimum_required(VERSION 3.14)
project(MyBenchmarks LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

include(FetchContent)
FetchContent_Declare(
    benchmark
    GIT_REPOSITORY https://github.com/google/benchmark.git
    GIT_TAG        v1.8.3
)
set(BENCHMARK_ENABLE_TESTING OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(benchmark)

add_executable(my_benchmarks
    benchmarks/sort_bench.cpp
    benchmarks/container_bench.cpp
)
target_link_libraries(my_benchmarks
    PRIVATE benchmark::benchmark benchmark::benchmark_main
)

Derleme ve çalıştırma:

cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)
./build/my_benchmarks

⚠️ Dikkat: Benchmark'ları her zaman Release modunda derle. Debug modda derleyici optimizasyonları kapalıdır, debug bilgisi ek overhead ekler — sonuçlar gerçek performansı yansıtmaz. -DCMAKE_BUILD_TYPE=Release unutulmamalı.


BENCHMARK Makrosu, DoNotOptimize ve Temel Kullanım

Google Benchmark'ın temel yapı taşlarını tek bir bütünleşik örnekte görelim. Bu örnek; basit benchmark, DoNotOptimize, parameterized benchmark (Range), fixture ve throughput raporlamayı bir arada gösterir:

#include <benchmark/benchmark.h>
#include <vector>
#include <algorithm>
#include <numeric>
#include <string>
#include <sstream>
#include <random>
#include <cmath>

// ============================================================
// 1) BENCHMARK MAKROSU — en basit hali
// ============================================================
// for (auto _ : state) döngüsü Google Benchmark'ın kalbidir.
// Framework, istatistiksel kararlılığa ulaşana kadar bu döngüyü
// otomatik olarak gerekli sayıda tekrarlar. Tekrar sayısını sen
// belirlemezsin — framework karar verir.

static void BM_VectorPushBack(benchmark::State& state) {
    for (auto _ : state) {
        std::vector<int> v;
        for (int i = 0; i < 1000; ++i) {
            v.push_back(i);
        }
        // Derleyici v'yi kaldırmasın
        benchmark::DoNotOptimize(v.data());
    }
}
BENCHMARK(BM_VectorPushBack);

// Reserve ile karşılaştır — ne kadar fark yapar?
static void BM_VectorPushBackReserved(benchmark::State& state) {
    for (auto _ : state) {
        std::vector<int> v;
        v.reserve(1000);
        for (int i = 0; i < 1000; ++i) {
            v.push_back(i);
        }
        benchmark::DoNotOptimize(v.data());
    }
}
BENCHMARK(BM_VectorPushBackReserved);

// ============================================================
// 2) DoNotOptimize ve ClobberMemory — derleyici tuzaklarını engelle
// ============================================================
// DoNotOptimize(x): Derleyiciye "x'in değeri kullanılıyor" der.
//   Gerçekte hiçbir şey yapmaz ama derleyicinin hesaplamayı
//   dead code olarak silmesini engeller.
// ClobberMemory(): "Bellekteki tüm değişkenler değişmiş olabilir"
//   sinyali verir — derleyici cache'lenmiş değerlere güvenemez.

static void BM_DeadCodeTrap(benchmark::State& state) {
    for (auto _ : state) {
        double x = 0;
        for (int i = 0; i < 1000; ++i) {
            x += std::sin(i * 0.01);
        }
        // x kullanılmıyor → derleyici tüm döngüyü silebilir!
        // Sonuç: "0 ns" → yanlış ölçüm
    }
}
BENCHMARK(BM_DeadCodeTrap);

static void BM_DeadCodeFixed(benchmark::State& state) {
    for (auto _ : state) {
        double x = 0;
        for (int i = 0; i < 1000; ++i) {
            x += std::sin(i * 0.01);
        }
        benchmark::DoNotOptimize(x);  // Hesaplama korunur
    }
}
BENCHMARK(BM_DeadCodeFixed);

// ============================================================
// 3) PARAMETERIZED BENCHMARK — farklı boyutlarla ölçekleme
// ============================================================
// state.range(0) ile parametre değerine eriş.
// RangeMultiplier(8)->Range(64, 1<<20):
//   64, 512, 4096, 32768, 262144, 1048576 boyutlarında çalışır.
// O(n), O(n log n), O(n²) karmaşıklıkları görselleştirir.

static void BM_Sort(benchmark::State& state) {
    auto n = state.range(0);

    for (auto _ : state) {
        state.PauseTiming();                // Setup'ı ölçme
        std::vector<int> data(n);
        std::iota(data.begin(), data.end(), 0);
        std::reverse(data.begin(), data.end());
        state.ResumeTiming();               // Asıl ölçümü başlat

        std::sort(data.begin(), data.end());
        benchmark::DoNotOptimize(data.data());
        benchmark::ClobberMemory();
    }

    // Throughput bilgisi: kaç eleman/saniye, kaç byte/saniye
    state.SetItemsProcessed(state.iterations() * n);
    state.SetBytesProcessed(state.iterations() * n * sizeof(int));
}
BENCHMARK(BM_Sort)->RangeMultiplier(8)->Range(64, 1 << 20);

// İki parametre: Args ile
static void BM_StringFind(benchmark::State& state) {
    auto str_len = state.range(0);
    auto pat_len = state.range(1);
    std::string haystack(str_len, 'a');
    std::string needle(pat_len, 'b');

    for (auto _ : state) {
        auto pos = haystack.find(needle);
        benchmark::DoNotOptimize(pos);
    }
}
BENCHMARK(BM_StringFind)
    ->Args({100, 5})->Args({1000, 5})->Args({10000, 5})
    ->Args({100, 50})->Args({1000, 50})->Args({10000, 50});

// ============================================================
// 4) FIXTURE BENCHMARK — ortak setup/teardown
// ============================================================
// Birden fazla benchmark aynı veri hazırlığını paylaştığında
// fixture sınıfı kullan. SetUp() her benchmark'tan önce,
// TearDown() sonra çağrılır.

class SortFixture : public benchmark::Fixture {
public:
    void SetUp(const benchmark::State& state) override {
        auto n = state.range(0);
        data.resize(n);
        std::mt19937 gen(42);  // Sabit seed → tekrarlanabilir
        std::uniform_int_distribution<int> dist(0, n * 10);
        for (auto& x : data) x = dist(gen);
    }

    void TearDown(const benchmark::State&) override {
        data.clear();
        data.shrink_to_fit();
    }

    std::vector<int> data;
};

BENCHMARK_DEFINE_F(SortFixture, FullSort)(benchmark::State& state) {
    for (auto _ : state) {
        auto copy = data;
        std::sort(copy.begin(), copy.end());
        benchmark::DoNotOptimize(copy.data());
    }
}
BENCHMARK_REGISTER_F(SortFixture, FullSort)
    ->RangeMultiplier(10)->Range(1000, 1000000);

// partial_sort: Sadece ilk %10'u sırala
BENCHMARK_DEFINE_F(SortFixture, PartialSort)(benchmark::State& state) {
    auto k = state.range(0) / 10;
    for (auto _ : state) {
        auto copy = data;
        std::partial_sort(copy.begin(), copy.begin() + k, copy.end());
        benchmark::DoNotOptimize(copy.data());
    }
}
BENCHMARK_REGISTER_F(SortFixture, PartialSort)
    ->RangeMultiplier(10)->Range(1000, 1000000);

// nth_element: Median bul
BENCHMARK_DEFINE_F(SortFixture, NthElement)(benchmark::State& state) {
    auto k = state.range(0) / 2;
    for (auto _ : state) {
        auto copy = data;
        std::nth_element(copy.begin(), copy.begin() + k, copy.end());
        benchmark::DoNotOptimize(copy.data());
    }
}
BENCHMARK_REGISTER_F(SortFixture, NthElement)
    ->RangeMultiplier(10)->Range(1000, 1000000);

// ============================================================
// 5) STRING BİRLEŞTİRME KARŞILAŞTIRMASI — gerçek dünya senaryosu
// ============================================================
static void BM_StringConcat_PlusEquals(benchmark::State& state) {
    auto n = state.range(0);
    for (auto _ : state) {
        std::string result;
        for (int i = 0; i < n; ++i) result += "hello";
        benchmark::DoNotOptimize(result.data());
    }
}
BENCHMARK(BM_StringConcat_PlusEquals)->RangeMultiplier(10)->Range(10, 100000);

static void BM_StringConcat_Stream(benchmark::State& state) {
    auto n = state.range(0);
    for (auto _ : state) {
        std::ostringstream ss;
        for (int i = 0; i < n; ++i) ss << "hello";
        std::string result = ss.str();
        benchmark::DoNotOptimize(result.data());
    }
}
BENCHMARK(BM_StringConcat_Stream)->RangeMultiplier(10)->Range(10, 100000);

static void BM_StringConcat_Reserve(benchmark::State& state) {
    auto n = state.range(0);
    for (auto _ : state) {
        std::string result;
        result.reserve(n * 5);
        for (int i = 0; i < n; ++i) result.append("hello", 5);
        benchmark::DoNotOptimize(result.data());
    }
}
BENCHMARK(BM_StringConcat_Reserve)->RangeMultiplier(10)->Range(10, 100000);

BENCHMARK_MAIN();

Bu tek dosya derlendiğinde framework tüm benchmark'ları çalıştırır ve güzelce formatlanmış bir tablo basar. Her önemli kavramı — temel makro, DoNotOptimize, parameterized, fixture, throughput raporlama — bir arada görüyorsun.


Sonuç Analizi: Çıktıyı Okumak

Yukarıdaki benchmark çalıştırıldığında şöyle bir tablo görürsün:

--------------------------------------------------------------
Benchmark                       Time        CPU   Iterations
--------------------------------------------------------------
BM_VectorPushBack             4523 ns    4510 ns       155212
BM_VectorPushBackReserved     1847 ns    1842 ns       379821
BM_DeadCodeTrap               0.32 ns    0.31 ns   1000000000
BM_DeadCodeFixed              8423 ns    8401 ns        83214
BM_Sort/64                    1203 ns    1198 ns       583914
BM_Sort/512                  13456 ns   13421 ns        52145
BM_Sort/4096               142300 ns  141987 ns         4931

Her sütunun anlamı:

SütunAçıklama
TimeWall-clock (gerçek) süre — OS scheduler gürültüsü dahil
CPUSadece process'in harcadığı CPU süresi
IterationsFramework'ün güvenilir sonuç için çalıştırdığı tekrar sayısı

CPU süresi genellikle Time'dan biraz düşüktür. Fark büyükse başka process'ler CPU çalıyor demektir — benchmark ortamı gürültülü.

BM_DeadCodeTrap'in 0.32 ns gösterdiğine dikkat et. Bu imkansız bir değer — sin() çağrısı tek başına onlarca nanosaniye sürer. Derleyici tüm döngüyü silmiş! DoNotOptimize eklediğimiz versiyonda gerçek değer: 8423 ns. Bu, dead code elimination'ın ne kadar tehlikeli olduğunun canlı kanıtı.

JSON Çıktı ve Karşılaştırma

Sonuçları makine-okunabilir formatta kaydetmek ve iki çalıştırmayı karşılaştırmak:

# JSON formatında kaydet
./my_benchmarks --benchmark_format=json --benchmark_out=baseline.json

# Optimizasyondan sonra tekrar çalıştır
./my_benchmarks --benchmark_format=json --benchmark_out=optimized.json

# Karşılaştır (Python aracı)
pip install google-benchmark
python -m google_benchmark.compare baseline.json optimized.json

# Sadece belirli benchmark'ları çalıştır
./my_benchmarks --benchmark_filter="BM_Sort.*"

# Çoklu tekrar + sadece istatistik raporu
./my_benchmarks --benchmark_repetitions=5 --benchmark_report_aggregates_only

--benchmark_repetitions=5 ile her benchmark 5 kez tekrarlanır ve ortalama/medyan/standart sapma raporlanır. Sonuçların tutarlılığını doğrulamak için ideal.


Micro-Benchmark Tuzakları

Micro-benchmark yazmak göründüğünden daha zor. Farkında olmadan tamamen yanlış sonuçlar üretebilirsin. En yaygın tuzaklar:

Dead Code Elimination

Yukarıdaki BM_DeadCodeTrap örneğinde gördük: derleyici, sonucu kullanılmayan hesaplamayı tamamen kaldırır. Çözüm: her zaman benchmark::DoNotOptimize() kullan.

Constant Folding

Derleyici, derleme zamanında hesaplanabilen ifadeleri sabit olarak katlar:

// Derleyici bunu derleme zamanında 42*17+3 = 717 olarak hesaplar
// Döngüdeki "hesaplama" aslında bir sabit ataması olur
int result = 42 * 17 + 3;
benchmark::DoNotOptimize(result);
// Çözüm: Girdinin runtime'dan geldiğinden emin ol
volatile int base = 42;  // volatile → derleyici sabit olarak göremez

Cache Warming ve Soğuk Başlangıç

İlk iterasyon soğuk cache ile çalışır — veri RAM'den okunur, yavaştır. Sonraki iterasyonlarda veri CPU cache'inde sıcaktır, çok daha hızlıdır. Google Benchmark otomatik olarak warm-up iterasyonları çalıştırır, ama çok büyük veri setleriyle çalışıyorsan cache'e sığmayacağını bilmelisin.

PauseTiming / ResumeTiming Overhead'i

state.PauseTiming() ve state.ResumeTiming() kullanışlıdır ama kendileri de maliyet ekler. Çok kısa benchmark'larda (nanosaniye seviyesi) bu overhead sonucu bozabilir. Mümkünse setup'ı döngü dışına taşı.

İzolasyon Yanılsaması

Micro-benchmark izole performansı ölçer. Gerçek uygulamada cache paylaşımı, bellek basıncı, thread contention gibi faktörler sonucu dramatik şekilde değiştirir. Micro-benchmark karşılaştırmalı bir araçtır: "A mı B mi daha hızlı?" sorusuna cevap verir. "Uygulamam yeterince hızlı mı?" sorusuna tek başına cevap veremez.

💡 İpucu: Micro-benchmark sonucuna göre karar verirken şunu sor: "Bu fark gerçek uygulamamda anlamlı mı?" 2 ns vs 5 ns fark, saniyede 10 çağrı yapılan bir fonksiyonda anlamsız ama saniyede 100 milyon çağrı yapılan bir inner loop'ta kritik.


perf ve Valgrind ile Profiling

Benchmark "ne kadar hızlı?" sorusunu cevaplar. Profiling ise "zaman nerede harcanıyor?" sorusunu cevaplar. İkisi birbirini tamamlar.

perf: Linux Performance Counters

Linux'ta en güçlü profiling aracı perf'tür. Hardware performance counter'larına doğrudan erişir, overhead'i çok düşüktür:

# Temel istatistikler: cycle, instruction, cache-miss, branch-miss
perf stat ./my_program
#   15,234,567  cycles
#    8,123,456  instructions     (0.53 IPC — düşük, stall var!)
#      234,567  cache-misses     (3.2%)
#       12,345  branch-misses    (0.8%)

# Fonksiyon bazında profiling
perf record -g ./my_program
perf report
# Hangi fonksiyon ne kadar CPU kullanıyor → interaktif görünüm

# Flame graph oluşturma
perf record -F 99 -g -- ./my_program
perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg
# flame.svg → tarayıcıda aç, görsel profil

IPC (Instructions Per Cycle) önemli bir metrik. 0.5 IPC görüyorsan CPU çoğu zaman bir şey bekliyordur — cache miss, branch misprediction veya bellek latency'si. 2.0+ IPC iyi bir değerdir.

Valgrind: Callgrind ve Cachegrind

Valgrind, programı simüle ederek çalıştırır. Gerçek zamanlama vermez ama instruction count ve cache davranışı konusunda kesin bilgi sağlar:

# Callgrind: Fonksiyon bazında instruction count + call graph
valgrind --tool=callgrind ./my_program
kcachegrind callgrind.out.12345   # GUI ile görselleştir

# Cachegrind: L1/L2/LL cache hit-miss analizi
valgrind --tool=cachegrind ./my_program
AraçNe ÖlçerOverheadPlatform
perfCPU cycles, cache miss, branch missDüşük (~2%)Linux
CallgrindInstruction count, call graphÇok yüksek (20-50x)Linux/macOS
CachegrindCache hit/miss oranlarıYüksek (10-20x)Linux/macOS
VTuneCPU, threading, vectorizationDüşükLinux/Windows
InstrumentsCPU, memory, I/OOrtamacOS

⚠️ Dikkat: Valgrind altında program 20-50x yavaşlar. Zamanlama ölçümleri geçersiz olur ama instruction count ve cache davranışı doğru kalır. Zamanlama için perf, yapısal analiz için Callgrind kullan — ikisini karıştırma.

Ne Zaman Hangisi?

Tipik iş akışı şöyle:

  1. Google Benchmark ile "hangi yaklaşım daha hızlı?" sorusunu cevapla

  2. perf stat ile genel resmi gör: IPC düşük mü, cache-miss yüksek mi?

  3. perf record + report veya flame graph ile darboğaz fonksiyonunu bul

  4. Callgrind ile o fonksiyonun iç yapısını detaylı analiz et

  5. Optimize et, tekrar benchmark yap, farkı ölç


Benchmark Sonuçlarını CI'da Takip Etme

Performansı bir kez ölçmek yetmez. Her commit'te otomatik ölçüp regresyon tespit etmek gerekir. Birisi "küçük bir refactor" deyip performansı %50 düşürebilir — bunu yakalayacak sistem lazım.

GitHub Actions ile Benchmark CI

# .github/workflows/benchmark.yml
name: Benchmark

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  benchmark:
    runs-on: ubuntu-22.04

    steps:
      - uses: actions/checkout@v4

      - name: Build (Release)
        run: |
          cmake -B build -DCMAKE_BUILD_TYPE=Release
          cmake --build build --target my_benchmarks -j$(nproc)

      - name: Run Benchmarks
        run: |
          ./build/my_benchmarks \
            --benchmark_format=json \
            --benchmark_out=results.json \
            --benchmark_repetitions=3 \
            --benchmark_report_aggregates_only

      - name: Track Results
        uses: benchmark-action/github-action-benchmark@v1
        with:
          tool: 'googlecpp'
          output-file-path: results.json
          github-token: ${{ secrets.GITHUB_TOKEN }}
          auto-push: true
          alert-threshold: '130%'
          comment-on-alert: true
          fail-on-alert: false
          benchmark-data-dir-path: docs/benchmarks

Bu pipeline'ın yaptıkları:

  1. Her push ve PR'da benchmark'ları Release modunda derleyip çalıştırır

  2. Sonuçları JSON olarak kaydeder

  3. benchmark-action ile GitHub Pages'e zaman serisi grafiği oluşturur

  4. Önceki sonuçla karşılaştırır — %30'dan fazla yavaşlama varsa PR'a otomatik uyarı yorumu ekler

  5. Zamanla performans trendini izlemeye olanak tanır

Sonuçları Yorumlama

Tipik bir PR karşılaştırması şöyle görünür:

PR #142: "Yeni cache katmanı eklendi"
--------------------------------------------------------------
Benchmark          Before        After       Change
--------------------------------------------------------------
BM_Lookup/1000      45 ns        12 ns       -73%  ✅
BM_Lookup/10000     89 ns        15 ns       -83%  ✅
BM_Insert/1000     120 ns       135 ns       +12%  ⚠️
BM_Insert/10000    450 ns       890 ns       +97%  ❌
--------------------------------------------------------------

Lookup hızlanmış ama Insert ciddi şekilde yavaşlamış — trade-off var. Bu bilgi olmadan "cache eklendi, performans arttı" demek eksik olurdu. Benchmark tam resmi gösterir.

CI'da Dikkat Edilmesi Gerekenler

CI runner'ları paylaşımlıdır. Aynı makinede başka job'lar çalışabilir, CPU frekansı değişebilir (turbo boost, termal throttling). Bu yüzden:

  • Mutlak değerlere değil göreceli değişime odaklan

  • --benchmark_repetitions=3 ile tekrar yap, varyansı azalt

  • Alert threshold'u çok dar tutma (%10 yerine %30 gibi) — CI noise'u false positive üretir

  • Kritik performans testleri için dedicated runner kullanmayı düşün


Özet

  • Benchmark olmadan optimizasyon yapmak, tahlilsiz ilaç yazmak gibidir — sezgi yanıltır, önce ölç sonra optimize et.

  • `std::chrono` ile manuel zamanlama basit durumlar için çalışır ama dead code elimination, cache warming, istatistik eksikliği ve boilerplate yükü yüzünden profesyonel kullanım için yetersizdir.

  • Google Benchmark, BENCHMARK makrosu ile otomatik tekrarlama, DoNotOptimize ile derleyici tuzaklarından korunma, Range/Args ile parameterized testler ve fixture ile paylaşımlı setup sunar.

  • Micro-benchmark tuzakları — dead code elimination, constant folding, cache warming — sonuçları tamamen geçersiz kılabilir; DoNotOptimize ve ClobberMemory bu tuzakları engeller.

  • perf düşük overhead'le zamanlama ve hardware counter analizi sağlarken, Callgrind detaylı instruction-level call graph ve cache analizi sunar — zamanlama ve yapısal analiz için ikisini birlikte kullan.

  • CI'da benchmark takibi ile her commit'te performans regresyonu otomatik tespit edilir — benchmark-action ile %30+ yavaşlama PR'a uyarı olarak eklenir.