这是使用 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);
使用存储在x
和y
中的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 吗? @KonradRudolphdouble 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】:总是有一堆注释要添加到堆栈内存的任何使用中。正如您所指出的,当空间用完时,堆栈的大小是有限的,并且会出现相当严重的不当行为。如果有保护页面,堆栈溢出可能会崩溃,但在某些平台和线程环境中,有时可能是静默损坏(坏)或安全问题(更糟)。
还请记住,堆栈 allocation 与 malloc
相比非常快(它只是堆栈指针寄存器的减法)。但那段记忆的用途可能不是。大量下推堆栈帧的副作用是您将要调用的叶函数的缓存行不再驻留。因此,对该内存的任何使用都需要进入 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 的好理由吗?的主要内容,如果未能解决你的问题,请参考以下文章