这是使用 alloca 的好理由吗?

Posted

技术标签:

【中文标题】这是使用 alloca 的好理由吗?【英文标题】:Is this a good reason to use alloca? 【发布时间】:2013-04-24 18:49:13 【问题描述】:

我有以下功能:

double 
neville (double xx, size_t n, const double *x, const double *y, double *work);

使用存储在xy 中的n 点在xx 处执行拉格朗日插值。 work 数组的大小为 2 * n。由于这是多项式插值,n 在大约 5 的范围内,很少超过 10。

此函数经过积极优化,应该在紧密循环中调用。分析表明在循环中分配工作数组的堆是不好的。不幸的是,我应该把它打包成一个类似函数的类,客户端一定不知道工作数组。

目前,我使用模板整数参数作为度数和 std::array 以避免动态分配 work 数组:

template <size_t n>
struct interpolator

    double operator() (double xx) const
    
        std::array<double, 2 * n> work;
        size_t i = locate (xx); // not shown here, no performance impact
                                // due to clever tricks + nice calling patterns

        return neville (xx, n, x + i, y + i, work.data ());
            

    const double *x, *y;
;

本来可以将工作数组存储为类的可变成员,但operator() 应该由多个线程同时使用。如果你在编译时知道n,这个版本是可以的。

现在,我需要在运行时指定 n 参数。我想知道这样的事情:

double operator() (double xx) const

    auto work = static_cast<double*> (alloca (n * sizeof (double)));
    ...

使用alloca 时会响起一些铃声:我当然会对n 设置上限以避免alloca 调用溢出(无论如何使用100 次多项式插值是非常愚蠢的)。

我对这种方法很不满意:

我是否错过了alloca 的一些明显危险? 这里有没有更好的方法来避免堆分配?

【问题讨论】:

你不能只用 C 编写这个函数并使用 C99 VLA 吗? @KonradRudolph double neville (double xx, size_t n, const double *x, const double *y, double *work); - 你需要运算符重载来编写这个函数吗?哇,我从来不知道! @H2CO3 呵呵,抓到我了。好吧,我的最后一个论点是我非常不喜欢链接 C 和 C++ 代码。当然没有真正的问题(如果做得好!我遇到过很多 C 库都做错了,给我带来了很多痛苦)。但是,我发现使用 VLA 比 alloca 带来的好处为零,但也许我错过了什么……? @KonradRudolph 受益:alloca() 可以在失败时调用 UB,VLA 符合 C99 标准,alloca() 甚至不是 POSIX,等等。 @H2CO3 阅读 ***.com/a/1018865/1968 上的 cmets。本质上,VLA 与alloca 具有完全相同的缺点,只是缺乏标准化。但是 GCC确实支持它,如果你想编写可移植代码,你可以自己提供 alloca.h(尽管缺乏标准化是一个好点,值得修改我的答案)。 【参考方案1】:

我对这种方法很不满意:

我是否遗漏了一些明显的 alloca 危险?

您指出了一个真正的危险:alloca 的堆栈溢出行为未定义。此外,alloca 实际上并没有标准化。例如,Visual C++ 有 _alloca 和 GCC by default defines it as a macro。但是,通过围绕少数现有实现提供一个薄包装器,可以相当容易地规避这个问题。

这里有没有更好的方法来避免堆分配?

不是真的。 C++14 将有一个(可能!)堆栈分配的可变长度数组类型。但在那之前,当您认为 std::array 不合适时,请在像您这样的情况下选择 alloca

不过,小问题:您的代码缺少alloca 的返回值的强制转换。它甚至不应该编译。

【讨论】:

匿名投票让我很伤心。有人愿意抚平我的伤口吗? 只是一个关于否决票的猜测:建议使用非标准功能,尤其是不说(在您的原始版本中),并且可能建议使用 C++ 中基本上是 C 库函数的东西有可能吸引纯粹主义者投反对票,而您两者都做到了。【参考方案2】:

总是有一堆注释要添加到堆栈内存的任何使用中。正如您所指出的,当空间用完时,堆栈的大小是有限的,并且会出现相当严重的不当行为。如果有保护页面,堆栈溢出可能会崩溃,但在某些平台和线程环境中,有时可能是静默损坏(坏)或安全问题(更糟)。

还请记住,堆栈 allocationmalloc 相比非常快(它只是堆栈指针寄存器的减法)。但那段记忆的用途可能不是。大量下推堆栈帧的副作用是您将要调用的叶函数的缓存行不再驻留。因此,对该内存的任何使用都需要进入 SMP 环境,以将缓存线带回独占(在 MESI 意义上)状态。 SMP 总线是一个比 L1 缓存更受限制的环境(!),如果您围绕这个问题向堆栈帧发送垃圾邮件,这可能是一个真正的可扩展性问题。

此外,就语法而言,请注意 gcc 和 clang(以及我相信的英特尔编译器)都支持 C99 可变长度数组语法作为 C++ 扩展。您可能根本不需要实际调用 libc alloca() 例程。

最后,请注意malloc 真的没有那么慢。如果您正在处理几十 KB 或更大范围的单个缓冲区,那么为您要对它们执行的任何工作提供服务所需的内存带宽将淹没malloc 的任何开销。

基本上:alloca() 很可爱,并且有它的用途,但除非你准备好一个基准来证明你需要它,否则你可能不需要并且应该坚持使用传统分配。

【讨论】:

您是否对缓存关联性做出特定假设?因为我不明白为什么动态内存应该将更少的页面带入缓存 - 事实上它应该接触更多,因为它必须访问堆内部数据结构。因此,更有可能导致叶函数使用的页面被驱逐。如果您担心这些页面一开始就没有缓存,我不明白为什么。在大量使用大量堆栈分配的程序中,这些堆栈页面将在缓存中变热。 函数neville 很小,不会调用任何人。分析器说,每次分配工作数组都会使我的实际operator()(使用std::array)的运行时间增加三倍。 work 的大小最大也只有几十个字节。不过,感谢您的洞察力。 Ben:这不是缓存占用,而是缓存行状态。存储到 L1 高速缓存行或从 L1 高速缓存行加载不需要本地 CPU 之外的流量,只要该行处于 E 状态。因此,在这些行之上调用叶函数可能会很快,而在将 14k 放入堆栈后调用相同的函数则不会。相反,CPU 必须首先将操作广播给所有其他 CPU,以允许它们的窥探逻辑看到它。对于快速调用的叶函数,这可能很重要。 @andy:我明白你在第一次通话时的意思。 (当然,这涉及保护页面、异常处理、更新 TLB ——以及将新页面带入独占统计信息是您最不担心的)但是什么会导致这些页面在此之后离开 E 状态?堆栈对于每个线程都是本地的,没有其他线程会声称拥有所有权。唯一导致缓存行失去排他性的原因是它是否被驱逐。并且本地堆栈缓冲区将导致更少的驱逐。抱歉,虽然我喜欢你想的方向,但这个答案是错误的。 SMP 逻辑不知道堆栈是“本地的”。它所知道的是 CPU 使用的线路与以前不同。再一次,你对“页面”的使用让我相信你误解了这个问题——我们谈论的是缓存硬件,而不是 MMU 行为。【参考方案3】:

这个怎么样:

double operator() (double xx) const

    double work_static[STATIC_N_MAX];
    double* work = work_static;
    std::vector<double> work_dynamic;

    if ( n > STATIC_N_MAX ) 
        work_dynamic.resize(n);
        work = &work_dynamic[0];
    

    ///...

没有不可移植的特性,异常安全,当n 太大时会优雅地降级。当然,您可以将work_static 设为std::array,但我不确定您从中看到了什么好处。

【讨论】:

有时我会错过明显的...我正在考虑禁止 n 的值大于 20(或某些预处理器常量,我的实际函数是在 y 参数上模板化的,因此源代码完整代码可用)。这值得一试,如果每次在堆栈上分配 320 字节不会降低性能。 @AlexandreC.:可以想象一个std::vector 实现,它带有一个“小字符串优化”,它包装了这种丑陋。 @AlexandreC。如果您认为堆分配可能是一个瓶颈,那么堆栈中不必要的大数组对 CPU 缓存的影响也可能是不可取的。但是,在真正的多线程负载下进行基准测试。 @hyde:当然。我不认为,我有测量堆分配是一个瓶颈。在这里,基准测试也非常重要。

以上是关于这是使用 alloca 的好理由吗?的主要内容,如果未能解决你的问题,请参考以下文章

有啥理由将 POCO 变成 Model 对象?

使用 if(1 || !Foo()) 有啥理由吗?

有任何理由使用 TINYTEXT 吗?

有啥理由不对函数使用 INLINABLE pragma 吗?

有啥理由这不是冗余代码吗?

有啥理由不放弃“var”吗?