字节反转方法的性能分析 - Google Benchmark 的挑战

问题描述 投票:0回答:1

(测试系统:CPU:第12代Intel(R) Core i7-1255U,16GB DDR4,操作系统:Windows 11 Pro,VS2022 x64版本,优化:/O0)

我想知道仅 CPU 内操作与带缓存的 ROM 获取之间的中间方式在哪里 - 并决定对其进行测试(假设需要处理 1GB)。我尝试了 3 种方法:第一个仅在 CPU 内,第二个是 CPU 内和 16 字节 LUT 的混合,第三个是 256 字节 LUT。

我运行了这个基准测试:(我是 Google Benchmark 的新手)

#include <array>
#include <benchmark/benchmark.h>


//
// ReverseByte by 3-pass shifts
//

[[nodiscard]] unsigned char ReverseByte3P(unsigned char byte)
{
    byte = static_cast<unsigned char>((byte & 0xF0) >> 4 | (byte & 0x0F) << 4); // i.e. 11110000, 00001111
    byte = static_cast<unsigned char>((byte & 0xCC) >> 2 | (byte & 0x33) << 2); // i.e. 11001100, 00110011
    byte = static_cast<unsigned char>((byte & 0xAA) >> 1 | (byte & 0x55) << 1); // i.e. 10101010, 01010101

    return byte;
}


//
// ReverseByte by nibble LUT
//

[[nodiscard]] constexpr unsigned char ReverseByteNL(const unsigned char byte)
{
    constexpr std::array<unsigned char, 16> reverse_nibble_lut{ // *Initialized at compile time*
        0x00, 0x08, 0x04, 0x0C, 0x02, 0x0A, 0x06, 0x0E,
            0x01, 0x09, 0x55, 0x0D, 0x03, 0x0B, 0x07, 0x0F
    };

    return static_cast<unsigned char>(reverse_nibble_lut[byte & 0x0F] << 4 | reverse_nibble_lut[byte >> 4]);
}


constexpr std::array kReverseByteLut{ // *Initialized at compile time* (**)
    []() constexpr {
        std::array<unsigned char, 0xFF + 1> lut{};
        for (uint16_t i = 0; i <= 0xFF; i++) {
            lut[i] = ReverseByteNL(static_cast<unsigned char>(i));
        }
        return lut;
    }()
};


//
// ReverseByte by Full LUT
//

[[nodiscard]] constexpr unsigned char ReverseByteLut(const unsigned char byte)
{
    return kReverseByteLut[byte];
}


// ** GOOGLE BENCHMARK: **

static void BM_Warmup(benchmark::State& state) {
    unsigned char byte{ 0 };
    for (auto _ : state) {
        const auto r = ReverseByte3P(byte++);
    }
}

static void BM_ReverseByte3P(benchmark::State& state) {
    unsigned char byte{ 0 };
    for (auto _ : state) {
        const auto r = ReverseByte3P(byte++);
    }
}

static void BM_ReverseByteNL(benchmark::State& state) {
    unsigned char byte{ 0 };
    for (auto _ : state) {
        const auto r = ReverseByteNL(byte++);
    }
}

static void BM_ReverseByteLut(benchmark::State& state) {
    unsigned char byte{ 0 };
    for (auto _ : state) {
        const auto r = ReverseByteLut(byte++);
    }
}

BENCHMARK(BM_Warmup)->Iterations(1000);
BENCHMARK(BM_ReverseByte3P)->Iterations(1000000);
BENCHMARK(BM_ReverseByteNL)->Iterations(1000000);
BENCHMARK(BM_ReverseByteLut)->Iterations(1000000);

BENCHMARK_MAIN();

--

昨天(一天结束时),我得到了这些结果(每次迭代的时间,CPU 时间):

BM_预热/迭代:1000 6.70 ns 0.000 ns
BM_ReverseByte3P/迭代:1000000 8.86 纳秒 0.000 纳秒 BM_ReverseByteNL/迭代:1000000 6.61 ns 0.000 ns
BM_ReverseByteLut/迭代:1000000 4.38 纳秒 0.000 纳秒

今天(早上),我得到了这些:

BM_预热/迭代:1000 7.10 ns 0.000 ns
BM_ReverseByte3P/迭代:1000000 4.61 纳秒 15.6 纳秒 BM_ReverseByteNL/迭代:1000000 9.42 ns 0.000 ns
BM_ReverseByteLut/迭代:1000000 4.52 纳秒 15.6 纳秒

...还有这个(另一个测试):

BM_预热/迭代:1000 4.20 ns 0.000 ns
BM_ReverseByte3P/迭代:1000000 2.99 纳秒 0.000 纳秒 BM_ReverseByteNL/迭代:1000000 7.94 ns 0.000 ns
BM_ReverseByteLut/迭代:1000000 3.25 纳秒 0.000 纳秒

--

同时,我也请Google Bard进行测量:

昨天

今天

--

首先,我还不清楚昨天的基准与今天有何不同。

其次,在ReverseByteNL(byte)中,LUT只有16字节,可以驻留在CPU缓存行中+它的计算量是ReverseByte3P(byte)的1/3。我没想到 ReverseByteNL 会是最慢的。昨天的结果对我来说似乎更合乎逻辑。

我的重点是这三个职能之间的相对结果。知道这是怎么发生的吗?

c++ caching c++17 performance-testing google-benchmark
1个回答
0
投票

首先,在没有优化的情况下对任何东西进行基准测试是没有意义的。未优化的代码更多地反映了代码的复杂性,而不是处理器可以执行的操作。实际上,经过优化后,一个 100 行函数可以编译为一条指令,另一个 10 行函数可以编译为 30 条指令。

现在,启用优化的问题是编译器试图聪明地删除无用的代码。而且基准测试代码通常是无用的。如果您像这样丢弃代码的结果,编译器将删除您的代码。为了避免这种情况,您可以将结果写入易失性,或者在大型数组上运行基准测试。

© www.soinside.com 2019 - 2024. All rights reserved.