xtensor 的 "operator/" 比 numpy 的 "/" 慢。

4

我正在尝试将之前用Python编写的一些代码转换为C++,目前我正在测试xtensor以查看它是否比numpy更快地完成我需要的任务。

我的其中一个函数接受一个方阵d和一个标量alpha,并执行逐元素操作alpha /(alpha + d)。背景:此函数用于测试哪个alpha值最好,因此它在循环中使用,其中d始终相同,但alpha不同。

以下所有时间尺度都是运行该函数100次的平均值。

在numpy中,这需要大约0.27秒,并且代码如下:

def kfun(d,alpha):
    k = alpha /(d+alpha)
    return k

但是xtensor需要大约0.36秒的时间,代码如下:
xt::xtensor<double,2> xk(xt::xtensor<double,2> d, double alpha){
    return alpha/(alpha+d);
}

我也尝试过使用 std::vector 来实现,但这不是我长期想要使用的,尽管它只花费了 0.22 秒。
std::vector<std::vector<double>> kloops(std::vector<std::vector<double>> d, double alpha, int d_size){
    for (int i = 0; i<d_size; i++){
        for (int j = 0; j<d_size; j++){
            d[i][j] = alpha/(alpha + d[i][j]);
        }
    }
    return d;
}

我注意到xtensor中的operator/ 使用了“惰性广播”,也许有一种方法可以使它立即执行吗?

编辑:

在Python中,使用以下方式调用该函数,并使用“time”包计时。

t0 = time.time()
for i in range(100):
    kk = k(dsquared,alpha_squared)
print(time.time()-t0)

在C++中,我调用函数如下,并使用计时器chronos进行计时:

//d is saved as a 1D npy file, an artefact from old code
auto sd2 = xt::load_npy<double>("/path/to/d.npy");

shape = {7084, 7084};
    xt::xtensor<double, 2> xd2(shape);
    for (int i = 0; i<7084;i++){
        for (int j=0; j<7084;j++){
            xd2(i,j) = (sd2(i*7084+j));
        }
    }

auto start = std::chrono::steady_clock::now();
for (int i = 0;i<10;i++){
    matrix<double> kk = kfun(xd2,4000*4000,7084);
}
auto end = std::chrono::steady_clock::now();
std::chrono::duration<double> elapsed_seconds = end-start;
std::cout << "k takes: " << elapsed_seconds.count() << "\n";


如果你想运行这段代码,我建议使用xd2作为一种对称的7084x7084随机矩阵,对角线上为零。
该函数的输出是一个名为k的矩阵,然后将在其他函数中使用,但我仍需要保持d不变,因为它稍后会被重复使用。
要运行我的C++代码,可以在终端中使用以下命令行:
cd "/path/to/src/" && g++ -mavx2 -ffast-math -DXTENSOR_USE_XSIMD -O3 ccode.cpp -o ccode -I/path/to/xtensorinclude && "/path/to/src/"ccode

提前感谢!


@TomdeGeus 您好!感谢您的评论。我想澄清一下,显然我对此还很陌生,但我认为如果我只指定大小而不是每次都让它计算,那么这个函数会更快?这样做有问题吗?这个函数在一个循环中被调用,具体来说是使用不同的alpha值。另外,您说我的std::vector示例没有分配返回值是什么意思?我知道您可以为对输入进行更改的函数创建一个void函数,例如,我是否不小心这样做了,而不是输出已更改的“d”? - abic011
编译器可能会优化掉(某些)size调用,但老实说,您的d_size选项已经超出了我的屏幕,所以我没有注意到它,并且假设您可能有一个打字错误。对于向量示例,您有一些未定义的d2,但是您已经纠正了这个问题,所以一切都很好! - Tom de Geus
关于这个问题,我有一个小评论。最新的编辑使问题变得更好了。更好的做法是确保它是可重现的:任何人都可以复制您的代码片段并直接编译和运行它。为此,您可以将“dsquared”和“xd2”引入为随机数矩阵。 - Tom de Geus
@TomdeGeus,你介意一下吗?我在编辑底部写的内容可以吗?还是我需要上传一个例子? - abic011
不用谢。为了以后的参考,我认为保持一致性还是很重要的,所以最好在这里进行编辑。 - Tom de Geus
显示剩余3条评论
1个回答

3
C++实现的一个问题可能是它会创建一个或者甚至两个临时副本,这可以避免。第一个副本由于未通过引用传递参数(或完美转发)而产生。不看代码的其余部分很难判断这是否对性能产生影响。如果保证在方法 xk() 之后不使用参数 d,编译器可能会将其移入方法中,但更可能为数据创建 d 的副本。
要通过引用传递参数,可以将方法更改为:
xt::xtensor<double,2> xk(const xt::xtensor<double,2>& d, double alpha){
    return alpha/(alpha+d);
}

为了使用完美转发(并且也启用其他xtensor容器,如xt::xarrayxt::xtensor_fixed),该方法可以更改为:
template<typename T>
xt::xtensor<double,2> xk(T&& d, double alpha){
    return alpha/(alpha+d);
}

此外,如果你能够避免为返回值保留内存空间,那将是可行的。当然,如果没有看到代码的其余部分,很难做出判断。但是,如果该方法在循环内部使用,并且返回值始终具有相同的形状,则可以受益于在循环外创建返回值并通过引用返回。为此,该方法可以进行如下更改:
template<typename T, typename U>
void xk(T& r, U&& d, double alpha){
    r = alpha/(alpha+d);
}

如果保证dr指向的内存不同,您可以进一步使用xt::noalias()包装r,以避免在分配结果之前进行临时复制。如果您不通过引用返回,则函数的返回值也是如此。

祝好运,编码愉快!


你好!感谢您的回答!我想澄清一下,因为我不认为我在问题中指定了,我需要保留原始的d矩阵,并输出一个名为“k”的矩阵,这是因为我基本上需要尝试许多不同的alpha值,以查看哪个给出了“最佳”结果。我相信您建议的方法不允许这样做,这正确吗?抱歉,这是我的第一个stackoverflow问题,我认为我没有提供足够的信息。 - abic011
1
对于我来说,在C++方面这个问题的重要之处在于@abic011发现向量示例更快,而原则上包含了您在此答案中提到的相同副本。因此,消除副本可能会解释与NumPy的差异,实际上也不会复制(如果@abic011可以确认,那就太好了,但不能使用std::vector版本。在拥有相同数量的副本的情况下,两者应该最多一样快/慢)。 - Tom de Geus
亲爱的@abic001,针对关于保持矩阵"d"不变的问题,这没有问题。您仍然可以将"d"作为const引用传递(我的第一个建议),方法调用将更快。矩阵"d"不会被函数改变。至于矩阵"k",也许您可以编辑您的问题以显示它的用法? - emmenlau
@TomdeGeus 我认为std::vector之所以比其他版本更快,是因为标准库有最快的元素访问方法,尽管我可能错了?抱歉,我不太确定您想让我确认什么,请您再解释一下吗? 不幸的是,@emmenlau将'd'作为'const'传递,始终需要大约0.38秒的时间,在我的笔记本电脑上,numpy目前需要大约0.28秒。 - abic011
1
非常抱歉!在复制时我打错了,直到现在才发现!实际上现在是0.2秒,好多了,非常感谢! - abic011
1
很高兴听到它解决了所有问题。只是为了确保不是 const 起作用,而是 &。后者强制对象通过引用传递(仅内存地址),而不是作为副本(在您的情况下为整个数据,即 4900 万条目)。 - Tom de Geus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接