以最大性能将双精度的向量截断为单精度

Truncate vector of doubles to single precision at maximum performance

本文关键字:单精度 精度 向量 性能 双精度      更新时间:2023-10-16

我正在流体动力学代码中试验降低某些操作中浮点数的精度,以测试是否真的需要双精度。

为此,我编写了一个截断函数,它将双精度向量的精度降低到单精度,而不转换数据。这使我能够评估某些函数的准确性,而无需将代码转换为单个精度。由于这些评估的计算成本很高,因此我的目标是拥有一个具有最高性能的截断函数。我已经尝试了以下内容,有什么方法可以提高truncate功能的性能吗?

#include <vector>
#include <iostream>
#include <iomanip>
#include <chrono>
#include <random>
void truncate(std::vector<double>& v)
{
    for (double& d : v)
    {
        float d_float = static_cast<float>(d);
        d = static_cast<double>(d_float);
    }
}
int main()
{
    std::random_device rd;
    std::mt19937 mt(rd());
    std::uniform_real_distribution<double> dist(0., 1.);
    const int n = 512*512*512;
    std::vector<double>v(n);
    for (double& d : v)
        d = dist(mt);
    std::cout << "Before: " << std::setprecision(15) << v[0] << std::endl;
    auto start = std::chrono::high_resolution_clock::now();
    truncate(v);
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::high_resolution_clock::now() - start);
    std::cout << "After:  " << std::setprecision(15) << v[0] << std::endl;
    std::cout << "Duration in microseconds: " << duration.count() << std::endl;
    return 0;
};
为了绝对

执行截断函数,您可能需要手动执行操作;假设您有权访问 OpenMP 和 SSE 内部函数,请查看_mm_cvtpd_ps(将 2 个双精度转换为两个浮点数)和_mm_cvtps_pd(转换回双精度)。

像这样:

double * vec; // aligned properly
#pragma omp parallel for schedule(static, 512)
for (int i = 0; i < size; i += 2)
{
    _mm_store_pd(vec + i, _mm_cvtps_pd(_mm_cvtpd_ps(_mm_load_pd(vec + i))));
}

这就是我会尝试的事情;你可以使用OpenMP选项,内部函数的确切形状(如果有的话,也许使用AVX)等。

编辑:AVX变体只是_mm256_cvtpd_ps等等,如果你可以实现这个建议,你也可以实现AVX版本。

如果您只想截断双精度以快速浮动,那么有更快(和更黑客)的方法可以做到这一点。根据您已经知道或可以假设的数字,它可以更快或更慢。

  • 你能有浮点尺度的畸形吗?
  • 你能有零吗?
  • 你能有NaN吗?
  • 浮点标度上会有无穷大的数字吗?

对于这个解决方案,我假设你可以有零,但没有非正规,NaN或无穷大。换句话说,我可以掩盖浮点数所没有的每一点,并获得足够接近的近似值:

for (double &d : doubles) { (*(uint64_t*)&d) &= 0xFFFF_FFFF_E000_0000; }

这保持你的符号位和指数,以及 23 位尾数。为了完全准确,您还需要裁剪指数 - 但它会导致不正常(我们假设不会发生)或无穷大(相同)。

请注意,告知处理器实际类型的解决方案可能更好、更准确。这是一篇解释性文章,以说明浮点数和双精度之间的实际区别。

您是否考虑过使用截断函数的多线程版本?像这样:

void truncate(std::vector<double>& v, const int n_threads = 1)
{
  if(n_threads <= 1) {
    for (double& d : v) {
      float d_float = static_cast<float>(d);
      d = static_cast<double>(d_float);
    }
  }
  else {
    std::vector<std::thread> threads;
    for (size_t id = 0; id < n_threads; ++id) {
      auto threadFunc = [=,&v]() {
        size_t beg = id*v.size()/n_threads;
        size_t end = std::min(v.size(), (id+1)*v.size()/n_threads + (id == n_threads-1)*(v.size() % n_threads));
        for (size_t i=beg; i < end; ++i) {
          float d = static_cast<float>(v[i]);
          v[i] = static_cast<double>(d);
        }
      };
      threads.push_back(std::thread(threadFunc));
    }
  for (auto & t : threads) t.join();
  }
}

对于大矢量,如果您能够负担得起使用许多线程,则增益应该很重要。

您是否考虑过使用普通的旧typedef(我更喜欢使用C++11的别名)作为using myType = float,然后使用std::vector<myType>作为您希望在代码中浮点的变量?这也将准确了解模拟的准确性和性能。

在这里宣传myType的使用需要一些时间,但值得 IMO,因为如果您愿意,您可以翻转回双倍。此外,正如@steiner所指出的,尽可能多地使用并行构造也会提高性能。