预先计算cos()和sin()的表格。

3

我想要提高我的动态链接库(DLL)的性能。

为此,我想使用cos()sin()的查找表,因为我会经常用到它们。

为了达到最佳性能,我想创建一个包含从0到2PI的结果cos和sin计算的表格。

为了在精度方面获得良好的结果,我认为每个函数1 MB的表格是大小和精度之间的良好平衡。

我想知道如何创建和使用这些表格,而不使用外部文件(因为它是一个DLL):我希望将所有内容都保留在一个文件中。

另外,我不想在插件启动时计算sin和cos函数:它们必须被计算一次并放入一个标准的向量中。

但是我该如何在C++中实现呢

EDIT1:jons34yp的代码非常适合创建向量文件。

我进行了小型基准测试,并发现如果你需要良好的精度和速度,可以使用250000个单位向量,并在线性插值之间,您将获得7.89E-11的最大误差(!),而且它比我尝试的所有近似方法都要快(确切地说,是sin()的13,296倍)。


https://dev59.com/qm445IYBdhLWcg3wapzF - fatihk
你可以编写一个辅助程序/脚本,以C++源代码语法生成表格。 - Angew is no longer proud of SO
1
您是否进行了剖析并确定这两个函数确实是代码中的热点?在现代处理器上,它们通常非常快速,您可能会发现基于表格的解决方案最终会变得较慢且缓存不友好。 - Retired Ninja
@ Thomas:这是针对Linux而不是Windows的。另外,我想知道如何从数据中获取向量,并以哪种格式存储数据。 - IonOne
@ninja:是的,它们是我代码中的热点。 - IonOne
3个回答

3

最简单的解决方案是编写一个单独的程序,创建一个包含您的向量定义的.cc文件。

例如:

#include <iostream>
#include <cmath>

int main()
{
    std::ofstream out("values.cc");

    out << "#include \"static_values.h\"\n"; 
    out << "#include <vector>\n";

    out << "std::vector<float> pi_values = {\n";
    out << std::precision(10);

    // We only need to compute the range from 0 to PI/2, and use trigonometric
    // transformations for values outside this range.
    double range = 3.141529 / 2;
    unsigned num_results = 250000;

    for (unsigned i = 0; i < num_results; i++) {
        double value = (range / num_results) * i;
        double res = std::sin(value);

        out << "    " << res << ",\n";
    }
    out << "};\n"
    out.close();
}

请注意,这样做不太可能提高性能,因为这么大的表格可能无法放入您的L2缓存中。这意味着大量三角计算需要访问RAM;每次这样的访问大约需要几百个CPU周期。
顺便说一下,您是否看过近似的SSE SIMD三角函数库。这看起来是它们的一个很好的使用案例。

你确定他的编译器能够处理具有250000个条目的表格吗?(我以前曾因为机器生成的代码而炸掉过编译器。) - James Kanze
@jons34yp:谢谢。你确定访问RAM这么慢吗?在我看来,一个周期是一个计算成本,但即使是处理小表格(我使用了一个4096个值的小表格来计算窗函数如Hanning或Hamming),访问RAM也会一直发生,而且速度很快,那为什么它会这么慢呢? - IonOne
@JamesKanze 至少GCC可以很好地处理这么大的表。 - user283145
@IonOne 是的,DRAM芯片的内部速度在过去20年基本上没有太大变化。增加的是CPU和RAM之间的带宽,但这并不能减少延迟。小表格没问题,因为它们驻留在L1或L2缓存中。即使访问大型表格,如果按顺序访问,也可以正常运行。但是,如果是随机访问大型表格(我认为这是这里的情况),确实会很慢。 - user283145
2
我使用了一些LUT进行快速测试,即使是使用线性插值,这比sin()函数要快得多!即使对于N=250000的表格也是如此。 - IonOne
显示剩余6条评论

3

您可以使用预计算而不是将它们已经预先计算好存储在可执行文件中:

double precomputed_sin[65536];

struct table_filler {
    table_filler() {
        for (int i=0; i<65536; i++) {
            precomputed_sin[i] = sin(i*2*3.141592654/65536);
        }
    }
} table_filler_instance;

这种方式在程序启动时只计算一次表格,而且它仍然位于固定的内存地址。之后,tsintcos可以作为内联实现。

inline double tsin(int x) { return precomputed_sin[x & 65535]; }
inline double tcos(int x) { return precomputed_sin[(x + 16384) & 65535]; }

65536次正弦计算几乎是我为1张图像必须执行的计算次数,因此这不是一个选项(顺便说一句,我写了我不想要即时计算表)。 - IonOne
@IonOne:我认为你在测量中犯了一些错误。预先计算一个包含65536个元素的正弦表比加载和初始化DLL要快得多。如果您不为每个图像重新初始化DLL,为什么需要针对每个图像进行此计算?顺便说一句,在我的PC上,使用4-对称性,计算正弦的65536项表大约需要0.3毫秒:你真的可以每秒加载和初始化DLL超过3000次吗? - 6502
好吧,也许你是对的,但是65536的精度不足以满足我的需求(也许使用线性插值...)。 - IonOne
2
@IonOne:对于这种表格查找,你总是使用插值。否则精度损失惊人。唯一需要考虑的是是否使用线性或更好的插值。 - MSalters
@MSalters:如果您在LUT中有足够的元素,则无需插值,因此您将拥有最大的计算速度。 - IonOne
显示剩余2条评论

0
通常回答这种问题的方法是编写一个小程序,生成一个带有表格中值的C++源文件,并将其编译成DLL。但是,如果您考虑到具有128000个条目的表格(128000个双精度数为1MB),则可能会遇到编译器内部限制的问题。在这种情况下,您可以将值作为内存转储写入文件,并在加载DLL时使用mmap映射该文件。(在Windows下,我认为您甚至可以将此第二个文件放入DLL文件的第二个流中,这样您就不必分发第二个文件。)

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