(测试系统: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 会是最慢的。昨天的结果对我来说似乎更合乎逻辑。
我的重点是这三个职能之间的相对结果。知道这是怎么发生的吗?
首先,在没有优化的情况下对任何东西进行基准测试是没有意义的。未优化的代码更多地反映了代码的复杂性,而不是处理器可以执行的操作。实际上,经过优化后,一个 100 行函数可以编译为一条指令,另一个 10 行函数可以编译为 30 条指令。
现在,启用优化的问题是编译器试图聪明地删除无用的代码。而且基准测试代码通常是无用的。如果您像这样丢弃代码的结果,编译器将删除您的代码。为了避免这种情况,您可以将结果写入易失性,或者在大型数组上运行基准测试。