You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何正确对查表与直接计算进行性能基准测试?

解决查表与直接计算的基准测试失真问题

问题背景

在对比查表实现与直接计算的性能时,Debug模式下查表速度是直接计算的4-5倍,符合预期;但开启-O2优化的Release模式下,查表速度比直接计算快数百倍,这与理论值(L1缓存查表约10周期,直接计算15-20周期,耗时应最多为查表的2倍)严重不符。核心问题是编译器优化了无实际副作用的计算循环,直接丢弃了未被使用的计算结果。

核心原因

编译器在-O2等优化级别下会执行死代码消除:如果循环内的计算结果未被后续代码使用(比如仅赋值给sign_of_product但未进一步处理),编译器会判定这段代码没有实际意义,直接删除整个循环,导致直接计算的耗时趋近于0,从而产生虚假的性能差距。

修正后的基准测试方案

1. 强制保留计算副作用

通过将计算结果累积到一个volatile变量中,让编译器无法判定代码是死代码。volatile关键字会告诉编译器该变量可能被外部修改,必须保留对它的所有操作。

示例:

// 定义volatile变量,确保结果被使用
volatile int8_t result_accumulator = 0;

在每个测试循环中,将结果累加到该变量:

// 单个点查表测试
for (int rept = 0; rept < REPETITIONS; rept++) {
    result_accumulator += lookup_table_sign[blade1][blade2];
}

// 单个点直接计算测试
for (int rept = 0; rept < REPETITIONS; rept++) {
    // 直接计算逻辑...
    result_accumulator += sign_of_product;
}

2. 修复数组越界bug

原测试中单个点的blade1 = rand() % 256,但lookup_table_sign的大小是SIZE=64,会导致数组越界(未定义行为),修正为:

uint8_t blade1 = rand() % SIZE;
uint8_t blade2 = rand() % SIZE;

3. 随机化测试输入

使用随机生成的输入数组,避免编译器对固定输入做常量折叠或提前计算:

// 提前生成大量随机测试点
#include <vector>
#include <utility>

// ...

vector<pair<uint8_t, uint8_t>> test_points(REPETITIONS);
for (auto& p : test_points) {
    p.first = rand() % SIZE;
    p.second = rand() % SIZE;
}

// 单个点查表测试
auto start = std::chrono::high_resolution_clock::now();
for (auto& p : test_points) {
    result_accumulator += lookup_table_sign[p.first][p.second];
}
auto end = std::chrono::high_resolution_clock::now();

4. 调整测试时长

确保每个测试的运行时间至少在几百毫秒以上,减少计时误差。可以根据实际情况增大REPETITIONS的值,比如调整为1000000甚至更高。

5. 隔离测试环境

每个测试之间可以添加缓存预热步骤,或者分开编译运行不同的测试用例,避免前一个测试的缓存状态影响下一个测试的结果。

修改后的完整基准测试代码片段

// ... 其他代码保持不变 ...

int main() {
    initialize_constants();

    // ... 生成blades数组、创建lookup_table的代码保持不变 ...

#pragma region benchmark
    volatile int8_t result_accumulator = 0;
    const long long REPETITIONS = 1000000; // 增大重复次数,保证测试时长足够

    // 生成随机测试点
    vector<pair<uint8_t, uint8_t>> test_points(REPETITIONS);
    for (auto& p : test_points) {
        p.first = rand() % SIZE;
        p.second = rand() % SIZE;
    }

    // benchmark single point with table lookup
    auto start = std::chrono::high_resolution_clock::now();
    for (auto& p : test_points) {
        result_accumulator += lookup_table_sign[p.first][p.second];
    }
    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double, std::milli> elapsed_miliseconds = end - start;
    cout << "Lookup table (random points) took " << elapsed_miliseconds.count() << " ms" << endl;

    // benchmark entire table with table lookup
    start = std::chrono::high_resolution_clock::now();
    int full_table_repeats = REPETITIONS / (SIZE * SIZE);
    for (int rept = 0; rept < full_table_repeats; rept++) {
        for (int row = 0; row < SIZE; row++) {
            for (int col = 0; col < SIZE; col++) {
                result_accumulator += lookup_table_sign[row][col];
            }
        }
    }
    end = std::chrono::high_resolution_clock::now();
    elapsed_miliseconds = end - start;
    cout << "Lookup entire table took " << elapsed_miliseconds.count() << " ms" << endl;

    // benchmark single point with direct calculation
    start = std::chrono::high_resolution_clock::now();
    for (auto& p : test_points) {
        uint8_t blade1 = p.first;
        uint8_t blade2 = p.second;
        uint8_t igc__blade1 = blade1 >> 1u;
        igc__blade1 ^= igc__blade1 >> 1u;
        igc__blade1 ^= igc__blade1 >> 2u;
        igc__blade1 ^= igc__blade1 >> 4u;

        uint8_t blade2__masked = igc__blade1 & blade2;
        uint8_t parity__2 = __builtin_popcount(blade2__masked & 0b11111110) & 1u;
        int8_t sign_of_product = 1 - 2 * parity__2;
        
        result_accumulator += sign_of_product;
    }
    end = std::chrono::high_resolution_clock::now();
    elapsed_miliseconds = end - start;
    cout << "Direct calculation (random points) took " << elapsed_miliseconds.count() << " ms" << endl;

    // benchmark entire table with direct calculation
    start = std::chrono::high_resolution_clock::now();
    for (int rept = 0; rept < full_table_repeats; rept++) {
        for (int blade1 = 0; blade1 < SIZE; blade1++) {
            for (int blade2 = 0; blade2 < SIZE; blade2++) {
                uint8_t igc__blade1 = blade1 >> 1u;
                igc__blade1 ^= igc__blade1 >> 1u;
                igc__blade1 ^= igc__blade1 >> 2u;
                igc__blade1 ^= igc__blade1 >> 4u;

                uint8_t blade2__masked = igc__blade1 & blade2;
                uint8_t parity__2 = __builtin_popcount(blade2__masked & 0b11111110) & 1u;
                int8_t sign_of_product = 1 - 2 * parity__2;
                
                result_accumulator += sign_of_product;
            }
        }
    }
    end = std::chrono::high_resolution_clock::now();
    elapsed_miliseconds = end - start;
    cout << "Direct calculation entire table took " << elapsed_miliseconds.count() << " ms" << endl;

    // 打印累加结果,防止编译器优化掉整个累加逻辑
    cout << "Accumulated result: " << (int)result_accumulator << endl;

#pragma endregion benchmark

    return 0;
}

内容的提问来源于stack exchange,提问作者tutizeri

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 04:47:07