何时使用std::complex<long double>而不是自己的复数数据类型(结构体等)?

3

我目前正在处理一个项目,之前的开发人员使用了

std::complex<long double>

该软件在大部分代码中都广泛使用了信号处理方法,这些方法都是使用上述复杂数据类型实现的。经常创建、访问和删除大型多维数组。

这种数据类型的好处是,所有所需的数学函数(例如来自<cmath>的函数)都支持复数,因此在使用此数据类型进行基本数学运算时几乎没有额外的开销。

我们的软件还通过该开发人员实现了其他函数,例如对大数据量的 n 维复卷积。

我目前正在开发一个需要大量使用 n 维卷积的扩展功能。然而,这个扩展的大多数情况不需要进行复杂运算。程序当前运行速度较慢,我在想在关键部分是否可以使用专有结构体来提高速度,类似于:

struct CPLX{
long double REAL;
long double IMAG;
}CPLX; 

并实现所需的方法(实现数学运算,如复杂乘法、相位等...)。

对于大多数扩展不需要复杂操作的部分:

(a+0i)*(b+0i)

a*b

慢得多吗?

使用自己的结构体与高效实现的数学运算和最小开销 VS. 使用 std::complexcmath 会更快吗? (除了需要额外的测试来确保一切正常之外)

使用 std::complex 会有明显的开销吗? 如果有的话,何时使用 std::complex 比使用自己的方法和结构体更合适?


我看不出使用自己的结构体会有任何帮助。但是为什么不使用std::complex来实现您的“高效”数学函数呢? - juanchopanza
5
如果你想要速度,自己实现并不是正确的方法。可以寻找Intel、AMD或Nvidia数学库,它们采用手动优化汇编语言,具有缓存阻塞、并行计算和SSE4等特性。 - Zan Lynx
1
这个问题太过笼统。不同实现的std::complex将会有不同的开销,特别是在具有不同内联能力的不同编译器上。 - Puppy
1
另一个有用的建议是,您可能不需要使用“long double”。相反,仔细检查您的浮点运算,以减少误差积累。您还可以研究我认为称为线性浮点表达式的内容。这是通过将值表示为类似于“2.510^9 + 3.710^3”的求和来获得更高的精度。 - Zan Lynx
但我正在处理的部分大多数情况下不需要复杂的操作...而其他需要的部分可以单独处理。因此,使用单独的方法计算会更快,为什么要使用完整的复杂操作(特别是在n维卷积中有很多乘法和加法的情况下)? - mmoment
5个回答

9

不要重复发明轮子

内置的标准库已经针对你的硬件进行了优化和调整。不要浪费时间去制作只能成为默认值一小部分的东西。如果您发现在特定例程上,剖面显示它很慢,请使用更好的库,例如Intel或GNU的复杂浮点数库。

编辑:不要害怕复杂数字库可能带来的开销。唯一的内存开销是将实部和虚部一起存储在对象中,而唯一的时间开销是实际打包它们在一起的时间。除非您从一开始就不需要复杂数字,否则您提出的实现都将复制这些操作。


请留下评论说明您为何要进行负面评价。 - randomusername
这是一个完全正确的答案,命中了基本目标。 - Puppy
我没有点踩...我同意不应该重复造轮子 :) 但我没有点踩。 - mmoment
使用复数进行纯实数据操作的开销如何?例如 (a + 0i)(b+0i) 和 ab 的比较? - mmoment
1
@mmoment 一个合适的实现应该意识到这一点,并包含适当的调度程序。 - randomusername

3
对于大部分不需要复杂操作的部分(这对我的扩展是真实的),(a+0i)<b+0i>ab速度慢得多。

是的,它会很慢(除非你使用-ffast-math编译)。但是你可以简单地写:

a.real() * b.real()

无需重写std::complex,它提供了所有你需要的方法。


2
看起来我是少数认为 std::complex 在某些关键内部循环中可能太慢的人之一,但无论如何,这是我的意见:一段时间以前,我正在编写一个简单的代码,用于计算三个变量的复合多项式并执行一些除法。我注意到,每当我使用相应的显式实值算法替换复数除法(更多)或乘法(更少)运算符重载(* 或 /)时,代码运行速度明显更快。在大多数乘法和除法被替换后,我相信速度提升约为 30 到 40%。虽然不是非常高,但对于如此有限且关键的代码来说,值得牺牲可读性。
这是在 GCC 上(我不记得版本号,但它是在 4.x 中),我检查了复杂的除法以了解为什么它会如此缓慢。结果发现,它会执行许多 Inf 和 NaN 的检查,以确保操作的边缘情况下正确的行为。当然,在进行数值操作时,当你得到 NaN 时,你已经迷失方向了,所以这种检查并不是真正想要的。我没有检查是否可以关闭此检查。

std::complex 的实际问题在于,按照标准,您不能将其用于任意类型,这可能是可以接受的,也可能会在最终必须在自己的类型上使用它时咬你一口(例如,对于 std::complex<std::complex<double>>,标准给出未定义行为)。 - Dmytry

1
作为Randomusername建议,我认为不要重复造轮子是最好的选择。
但是,如果大多数情况下不需要使用复杂操作,为什么不使用std::complex的包装器来实现一个类,用于两种类型的数字(实数和复数),一个对象作为实数,另一个作为复数,使用相同的接口(继承会有一些开销,但由于区分实数-更快-和复数域的最小变化而具有良好的性能)。更好地解释代码:
template < typename T>
class number
{
   //operators declarations
   //example
   //virtual number& operator= (const T& val) = 0;
   //virtual number& operator+= (const T& val) = 0;
   //virtual number& operator-= (const T& val) = 0;
   //virtual number& operator*= (const T& val) = 0;
   //virtual number& operator/= (const T& val) = 0;
};
template < typename T>
class real : public number<T>
{
    T number;
   //operators declarations

   // number& operator= (const T& val);
   // number& operator+= (const T& val);
   // number& operator-= (const T& val);
   // number& operator*= (const T& val);
   // number& operator/= (const T& val);
};
template < typename T>
class owncomplex :public number<T>
{
    std::complex<T> _complex;
    //operators declarations

   // number& operator= (const T& val);
   // number& operator+= (const T& val);
   // number& operator-= (const T& val);
   // number& operator*= (const T& val);
   // number& operator/= (const T& val); 
};

使用std::complex实现并通过字面值操作优化来改进实际操作,重写任何运算符都很困难。

1
自己编写结构并手写操作只会使你的代码更难读且难以维护。你真正想要的是利用 SSE/AVX 指令来加速。最好的方法是: - 使用类似 Intel 的 MKL 这样有许可费用但非常快的库 - 查看 Agner Fog 的向量库和他的优化手册 - 研究如何编写能够轻松被编译器优化为 SSE/AVX 指令的代码
还值得注意的是,这些操作可以通过多线程大大加速,最容易的方法是使用支持相应指令的自动并行编译器或者通过一些 OpenMP(如果你没用过的话,这是一个非常有帮助的库)。
最后,你可以通过内部库编写自己的 SSE/AVX 代码,但这非常耗时且使代码难以维护。此外,除非你正在做一些不能轻松使用像 MKL 这样的工具实现的非常棘手的事情,否则你可能不会得到很好的速度提升,除非你真的知道你在做什么。

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