这是将 64b 值的缓冲区重新格式化为 16b 的最快方法吗?

Is this the fastest way to reformat a buffer of 64b values to 16b?

提问人:Douglas B 提问时间:6/29/2023 更新时间:6/29/2023 访问量:83

问:

我有一个数据流,它将物理上的 64 位值输出到缓冲区。当缓冲区达到一定水平时,需要将其重新格式化为连续的 16 位值。实际值永远不会超过数据流生成的每个值的 64 位中的 24 位,因此这相当于将 24b 值截断为 16b 并重新排列缓冲区,以便这些值现在是连续的。我相信我已经找到了最快的方法来做到这一点,但是我不确定是否有我可能缺少的优化或 C++ 标准实用程序提供的更快方法。下面是一个 MRE,显示了我的重新格式化功能以及一个测试工具,用于生成我遇到的数据并计时重新格式化。

#include <iostream>
#include <chrono>
#include <unistd.h>

int num_samples = 160000;

void fill_buffer(uint8_t** buffer){
  *buffer = (uint8_t*)malloc(num_samples * sizeof(uint64_t));
  for (int i = 0; i < num_samples; i += 8){
    (*buffer)[i] = rand() % 0xFF;
    (*buffer)[i + 1] = rand() % 0xFF;
    (*buffer)[i + 2] = rand() % 0xFF;
  }
}

void reformat_1(uint8_t* buf){
  uint64_t* p_8byte = (uint64_t*)buf;
  uint16_t* p_2byte = (uint16_t*)buf;

  for (int i = 0; i < num_samples; i++){
    p_2byte[i] = p_8byte[i] >> 8;
  }
}

int main(int argc, char const* argv[]){
  uint8_t* buffer = NULL;

  fill_buffer(&buffer);
  auto start = std::chrono::high_resolution_clock::now();
  reformat_1(buffer);
  auto stop = std::chrono::high_resolution_clock::now();
  auto duration = std::chrono::duration_cast<std::chrono::microseconds>(stop - start);
  std::cout << "Time taken by function one: " << duration.count() << " microseconds" << std::endl;

  return 0;
}

我也愿意听到关于我的基准测试设置的反馈,我发现有趣的是,我从文件中读取的实际样本数据得到了~130uS,而对于随机生成的数据,我看到接近1800uS,所以这显然不是一个完全具有代表性的例子。-O3

我要注意的另一件事是,我认为会对我的实际时间(与合成时间相比)起作用,但显然不是:虽然这里是一个神奇的数字,但实际上,它是计算出来的,通常是常数(并非总是如此),但不是编译器会用常量替换的东西展开循环等(我认为)。num_samples

C++ 优化 微基准测试

评论

2赞 Some programmer dude 6/29/2023
您的函数将保留您创建的数组中的许多元素未初始化。这些未初始化的元素将具有不确定的值。在 C++ 中以任何方式使用不确定值会导致未定义的行为。fill_buffer
4赞 Some programmer dude 6/29/2023
除此之外,您的代码比 C++ 更像 C。你不应该在 C++ 中使用。你不应该需要在 C++ 中进行 C 样式转换(这样做通常表明你做错了什么)。不应使用指针来模拟 C++ 本身具有的引用。为什么不使用标准的 C++ 类和算法?malloc
0赞 Ted Lyngmo 6/29/2023
这感觉更像是代码审查,而不是试图解决特定问题
0赞 Jesper Juhl 6/29/2023
malloc? ?C型演员表? 而不是?双指针?此代码可以使用严肃的“现代 C++”更新。randNULLnullptr
0赞 Douglas B 6/29/2023
@Someprogrammerdude很好,我不知道这是未定义的,因为从技术上讲,我从不直接访问这些元素,我会想吗?到目前为止,它运行良好,没有任何警告或错误,所以我要感谢编译器。我想知道这是否是合成数据性能较慢的原因,我可以看到投射导致它缓存的数据比需要的多。总的来说,我更担心重新格式化缓冲区的功能,但当我在 PC 上时,我会编辑它以最初将缓冲区归零。需要明确的是,缓冲区在实践中从来都不是这样的 uninit,只是这个尝试的 MRE

答:

1赞 olegarch 6/29/2023 #1

这种微改进将性能提高了 ~10%:

void reformat_2(uint8_t* buf){
  uint32_t* p_8byte = (uint32_t*)buf;
  uint16_t* p_2byte = (uint16_t*)buf;
  uint16_t* p_2end  = p_2byte + num_samples;

  while(p_2byte < p_2end){
    *p_2byte++ = *p_8byte >> 8;
    p_8byte += 2;
  }
}

为了查看更清晰的数字,我将缓冲区大小增加了 100 倍,达到 16M 个条目。

评论

0赞 Douglas B 6/29/2023
有趣的是,你能解释一下你是怎么走到这一步的/什么推理会让你期望这更快吗?我将不得不查看程序集输出,但希望获得一些见解
1赞 olegarch 6/30/2023
1. 将索引和循环计数器合并在一起。2. 通过 32 位字而不是 64 位字访问 RAM,这需要更少的总线环路。p_2byte