与 numpy.logspace() 函数相比,如何用 C++ 编写快速代码?

Posted

技术标签:

【中文标题】与 numpy.logspace() 函数相比,如何用 C++ 编写快速代码?【英文标题】:How to write a fast code in C++ compared to numpy.logspace() function? 【发布时间】:2019-09-15 12:15:01 【问题描述】:

这是在 Python 中快速生成日志空间值的代码:

import numpy
print(numpy.logspace(0,1,num=10000000))

我尝试在 C++ 中模拟它的输出,如下:

#include <iostream>
#include <cmath>
#include <vector>
std::vector<double> logspace (const double &a, const double &b, const int &k)

    std::vector<double> logspace;
    for (int i = 0; i < k; i++)
    
        logspace.push_back(pow(10, i * (b - a) / (k - 1)));
    
    return logspace;

void logspace_print (std::vector<double> logspace)

    for (auto ls : logspace)
    
        std::cout << ls << "\n";
    
    std::cout << "\n";

int main ()

    std::vector<double> my_ls = logspace(0, 1, 10000000);
    logspace_print(my_ls);

放弃浮点运算,使用函数pow(., .)for-loop(可能还有很多其他原因),使我的代码变得幼稚,例如它的运行时非常微弱到 Python 的那个。我也在Is there something like numpy.logspace in C++? 看到了建议。但是,没有明显的差异。那么,如何修改我的代码或编写一个与 python 版本相媲美的新代码?

【问题讨论】:

你可以一直看numpy的源码吗? 当然。为什么不。但是,正如您也可以看到它的来源,有一系列相互关联的函数需要寻找。 【参考方案1】:

有趣的问题!我的答案在顶部有不同版本的功能。以下只是基准测试代码。使用 google-benchmark 作为库。

我的中间结果也可以在这里找到:1Quick-Bench.com 通常是一个很棒的网站。 您没有说是否要将打印到标准输出作为您的用例的一部分。印刷通常很昂贵。你避免std::endl 的同花顺,这很好!此外,printf 可能比 std::cout 更快。另请查看 fmtlib 2。它快速且易于使用。 通常,Numpy 使用的方法是最快的。 (在我的版本中命名为logspace_v3。)它包括首先运行linspace,然后就地取10 的幂。 不过,我仍然强烈地觉得我在这里缺少很多东西。使用适当的标志(-march=native -mtune=native 和快速数学)矢量化应该开始。但我不相信它会。这是一些带有矢量化的 Godbolt(第 590 行)3。 禁食的是摆脱pow 电话。请注意,这会累积浮点错误并导致结果不准确。 次要:通过 const 引用传递双精度或整数没有任何好处。

#include <algorithm>
#include <benchmark/benchmark.h>
#include <cmath>
#include <iostream>
#include <numeric>
#include <vector>

#include <gtest/gtest.h>

std::vector<double> logspace(double a, double b, int k) 
  std::vector<double> logspace;
  for (int i = 0; i < k; i++) 
    logspace.push_back(pow(10, i * (b - a) / (k - 1)));
  
  return logspace;


// Pre-allocate the correct size using .reserve()
std::vector<double> logspace_v1(double a, double b, int k) 
  std::vector<double> logspace;
  logspace.reserve(k);
  for (int i = 0; i < k; i++) 
    logspace.push_back(pow(10, i * (b - a) / (k - 1)));
  
  return logspace;


/// Manually extract the constant factor.
std::vector<double> logspace_v2(double a, double b, int k) 
  std::vector<double> logspace;
  logspace.reserve(k);
  const auto exp_scale = (b - a) / (k - 1);
  for (int i = 0; i < k; i++) 
    logspace.push_back(pow(10, i * exp_scale));
  
  return logspace;


/// Copy the impl behavior of numpy.linspace: First linspace then power.
std::vector<double> logspace_v3(double a, double b, int k) 
  /*
  y = linspace(start, stop, num=num, endpoint=endpoint, axis=axis)
  if dtype is None:
      return _nx.power(base, y)
  return _nx.power(base, y).astype(dtype, copy=False)
  */
  const auto exp_scale = (b - a) / (k - 1);
  std::vector<double> logspace;
  logspace.reserve(k);
  for (int i = 0; i < k; i++) 
    logspace.push_back(i * exp_scale);
  
  std::for_each(logspace.begin(), logspace.end(),
                [](double &x)  x = pow(10, x); );
  return logspace;


/// Improve on v3 by applying pow directly
std::vector<double> logspace_v4(double a, double b, int k) 
  const auto exp_scale = (b - a) / (k - 1);
  std::vector<double> logspace(k, 0.);
  std::generate(logspace.begin(), logspace.end(),
                [n = -1, exp_scale]() mutable 
                  n++;
                  return pow(10, n * exp_scale);
                );
  return logspace;


/// Use generate_n : First linspace then power.
std::vector<double> logspace_v5(double a, double b, int k) 
  const auto exp_scale = (b - a) / (k - 1);
  std::vector<double> logspace(k, 0.);
  std::iota(logspace.begin(), logspace.end(), 0);
  std::for_each(logspace.begin(), logspace.end(),
                [exp_scale](double &x)  x *= exp_scale; );
  std::for_each(logspace.begin(), logspace.end(),
                [](double &x)  x = pow(10, x); );
  return logspace;


std::vector<double> logspace_v6(double a, double b, int k) 
  const auto exp_scale = (b - a) / (k - 1);
  const auto factor = pow(10, exp_scale);
  std::vector<double> logspace;
  logspace.reserve(k);

  // val = pow(b, i * exp_scale);
  // = pow(pow(b, exp_scale), i);
  // = pow(f, i); with f := pow(b, exp_scale);
  // next = cur * f;
  // first = pow(b, a);

  double val = pow(10, a);
  for (int i = 0; i < k; i++) 
    logspace.push_back(val);
    val *= factor;
  
  return logspace;


template <std::vector<double> (*F)(double, double, int)>
static void LogspaceBench(benchmark::State &state) 
  for (auto _ : state) 
    benchmark::DoNotOptimize(F(0, 1, state.range(0)));
  

BENCHMARK_TEMPLATE(LogspaceBench, logspace)->Arg(1000);
BENCHMARK_TEMPLATE(LogspaceBench, logspace_v1)->Arg(1000);
BENCHMARK_TEMPLATE(LogspaceBench, logspace_v2)->Arg(1000);
BENCHMARK_TEMPLATE(LogspaceBench, logspace_v3)->Arg(1000)->Arg(10000000);
BENCHMARK_TEMPLATE(LogspaceBench, logspace_v4)->Arg(1000);
BENCHMARK_TEMPLATE(LogspaceBench, logspace_v5)->Arg(1000);
BENCHMARK_TEMPLATE(LogspaceBench, logspace_v6)->Arg(1000)->Arg(10000000);

class LogspaceTest
    : public testing::TestWithParam<
          std::function<std::vector<double>(double, double, int)>> ;

TEST_P(LogspaceTest, IsSame) 
  auto func = GetParam();
  const auto actual = func(0, 1., 1000);
  const auto expected = logspace(0., 1., 1000);
  // TODO: Buggy with (3, 70, 1000) and (0, 1, 1000)
  ASSERT_EQ(expected.size(), actual.size());
  for (int i = 0; i < expected.size(); i++) 
    ASSERT_DOUBLE_EQ(actual[i], expected[i]) << i;
  


INSTANTIATE_TEST_SUITE_P(InstantiationName, LogspaceTest,
                         testing::Values(logspace, logspace_v1, logspace_v2,
                                         logspace_v3, logspace_v4, logspace_v5,
                                         logspace_v6));

int main(int argc, char **argv) 
  ::benchmark::Initialize(&argc, argv);
  ::benchmark::RunSpecifiedBenchmarks();
  ::testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();

【讨论】:

Printf 只有在同步时才比 cout 快(默认情况下是这样)。您可以使用 std::ios::sync_with_stdio(false);禁用它。 "打印通常很昂贵。"打印到文件很昂贵。打印到 终端 需要很长时间。这就是为什么我敢打赌 Python 和 C++ 的性能相当的原因。将输出通过管道传输到文件中,以便更好地比较语言本身。现在他可能正在分析终端。 我没有看过我的答案中的印刷。已经太久了。 @Unapiedra,我很享受您的好回答(此外,感谢您推荐 Google 基准测试。听起来很棒)。 @Unapiedra & @Mooing Duck,我理解在运行时打印的真正糟糕的性能(以及效果)(更准确地说,Python 正在使用一种狡猾的技巧来打印。确实,它不会打印所有输出;))。无论如何,我有一个关于提高for-loop 性能的想法。在尝试之前,我想研究一下它的理论:将区间[0, 10000000]划分为(例如)[0, 1000] U [1000, 10000] U ... U [1000000, 10000000]并对其应用for循环是否是一种好方法?【参考方案2】:

至少可以对所示代码进行三个明显的优化。

1) 从logspace 返回时,以 C++17 模式编译以保证复制省略。

2)

std::vector<double> logspace;
for (int i = 0; i < k; i++)

使用logspace.reserve() 预分配向量,以避免在填充此向量时进行无用的重复重新分配。

3)

void logspace_print (std::vector<double> logspace)

此处按值传递会创建向量的完整副本,但没有任何用处。更改此函数,使其通过引用获取logspace 参数。

有一种可能的微优化可能会或可能不会产生任何影响:

logspace.push_back(pow(10, i * (b - a) / (k - 1)));

这个公式的“(b-a)/(k-1)”部分是常数,可以展开出循环。不过,我希望编译器自行完成,这是一个相当基本的优化。

【讨论】:

我应用了 2)、3) 和你的微优化建议(不幸的是,我还不能打开 c++17-compiler)。但是,差异小于 1 秒(我在 Linux 中使用time 命令来比较运行时间)。 我会给 +1,除非你忽略了终端的性能。

以上是关于与 numpy.logspace() 函数相比,如何用 C++ 编写快速代码?的主要内容,如果未能解决你的问题,请参考以下文章

NumPy 从数值范围创建数组

Js Deferred/Promise/Future 与 Scala 等函数式语言相比

为啥没有参数的函数(与实际函数定义相比)编译?

main() 函数与 C 中的其他函数相比如何?

Java的基础知识

与没有函数包装器的查询相比,SQL 函数非常慢