堆栈与缓存友好分配器
Stack vs cache friendly allocator
几天前,我开始处理缓存友好的代码,并滚动了一些不同的结构来确定如果我将变量放在堆栈或堆上,性能如何变化,以及不同的内存布局如何与线性任务(如迭代和搜索)一起扩展。
我不是在处理分配时间,只是在处理性能。
测试不准确,但至少应给出一些相关的数字,性能可能有何不同。
首先,我比较了 std::array 和向量的性能。
数组的测试代码:
int main()
{
std::array<mango::int16, 5000000> v;
mango::delta_timer timer; //simple timer class
for (int i = 0; 5000000 > i; ++i)
{
v[i] = i; //I know that i will overflow but that's no problem in this case
}
timer.start();
mango::for_each(v.begin(), v.end(), [](mango::int16& i)->void {++i; });
timer.stop();
std::cout << (double)timer.totalTime();
mango::mgetch(); /*crossplatform wrapper for _getch() --> supposed to
give me a point where I can exit the program without printing the results*/
mango::for_each(v.begin(), v.end(), print); /*print the entire
vector and hope that this will prevent the compiler from optimizing the array away*/
return 0;
}
常规向量的代码:
int main()
{
std::vector<mango::int16> v;
v.reserve(5000000);
mango::delta_timer timer;
for (int i = 0; 5000000 > i; ++i)
{
v.push_back(i);
}
timer.start();
mango::for_each(v.begin(), v.end(), [](mango::int16& i)->void {++i; });
timer.stop();
std::cout << (double)timer.totalTime();
mango::mgetch();
mango::for_each(v.begin(), v.end(), print);
return 0;
}
数组上的for_each需要 0.003 到 0.004 秒,矢量上的for_each在 0.005 到 0.007 秒之间。
在第一次测试之后,我推出了一个非常纤薄和简约的分配器,以尝试是否可以在堆栈内存中获得类似的性能。
分配器如下所示:
class block_allocator
{
public:
block_allocator(mango::int32 n, mango::int32 bsize, mango::int32 id)
: m_Memory(new mango::byte[n * bsize]), m_Capacity(n), m_BlockSize(bsize), m_ID(id), m_Blocks(n)
{
for (mango::byte* iterator = (mango::byte*)m_Memory; ((mango::byte*)m_Memory + n * bsize) > iterator; iterator += bsize)
{
m_Blocks.push_back(iterator);
}
}
~block_allocator()
{
delete[](mango::byte*)m_Memory;
m_Memory = nullptr;
}
void* allocate(mango::uint32 n)
{
if (m_Blocks.empty())
{
throw mango::exception::out_of_range(mango::to_string(m_ID) + std::string(" allocator went out of range"), "out_of_range");
}
void* block = m_Blocks.back();
m_Blocks.pop_back();
return block;
}
void deallocate(void* target)
{
if (m_Blocks.size() == m_Capacity)
{
delete[](mango::byte*)target;
}
m_Blocks.push_back(target);
}
private:
void* m_Memory;
mango::int32 m_Capacity;
mango::int32 m_BlockSize;
mango::int32 m_ID;
std::vector<void*> m_Blocks;
};
它只是一个非常简单的测试样本,不适合生产使用!
这是我使用分配器的测试示例:
int main()
{
std::array<mango::int16*, 5000000> v;
mango::delta_timer timer;
for (int i = 0; 5000000 > i; ++i)
{
v[i] = allocate_int(i); //allocates an int with the allocator
}
timer.start();
mango::for_each(v.begin(), v.end(), [](mango::int16* i)->void {++(*i); });
timer.stop();
std::cout << (double)timer.totalTime();
mango::mgetch();
mango::for_each(v.begin(), v.end(), print);
return 0;
}
在本例中,for_each的性能下降到 0.003 和 0.004 之间,就像第一个数组示例一样。
我知道,这些例子都没有清理
。所以这里有一个问题:由于我必须在 Visual Studio 2015 中增加堆栈大小才能运行此示例(否则会发生堆栈溢出),并且堆栈会随着大小的增加而变慢,因此缓存友好代码的可取方法是什么?
使用缓存友好的分配器将对象保持在堆上靠近在一起,可以达到与使用堆栈相同的性能(这可能在不同的示例中有所不同,但我认为即使接近堆栈性能对于大多数程序也足够了)。
构建一个适当的分配器并将大量内容存储在堆上并保持较低的"实际"分配计数而不是过度使用堆栈不是更有效吗?我问这个是因为我在互联网上经常阅读"尽可能频繁地使用堆栈",我担心这种方法并不像很多人想象的那么简单。
谢谢。
不要高估将所有内容保留在堆栈上的缓存值。是的,新分配的对象适合已经缓存的行是很好的。但是在例如 Haswell 上,缓存行只有 64 字节,因此就缓存而言,您很快就会耗尽连续性。(缓存集分发有一些好处,但这是一个次要的好处。如果你正在编写的代码实际上可以从额外的缓存一致性中受益,那么你通常使用大型数组,无论它们在哪里都是连续的。
我认为,"使用堆栈,而不是堆"的建议是建议您避免间接。
综上所述,假设LIFO分配模式并从中受益的单独分配器有一些小的好处。但它来自降低的簿记成本,而不是缓存友好性。
- cmake更新缓存的变量
- 试图对缓存进行跨步测试,但程序并没有结束
- 当有分配器意识的容器被复制/移动时,反弹分配器是否被复制/移走
- 将 std::allocate_shared 与多态资源分配器一起使用
- 缓存std::数组的选定元素,并在c++中自动保持其一致性
- 通过ccmake在cmake中缓存依赖选项
- 尝试将lambda函数放在队列中时出现一般分配器错误(可能是与unique_ptr有关的错误)
- C++17 - 使用自定义分配器的节点提取/重新插入 - 适用于 clang++/libc++,但不适用于 libstd
- 使用宏扩展的泛型:为什么指令缓存使用不当?
- 使用 std::分配器在 constexpr 中进行默认初始化
- 使用不兼容的分配器复制分配无序列图
- 如何使缓存线程安全
- C++:矢量分配器行为、内存分配和智能指针
- 存储指令是否会阻止缓存未命中的后续指令?
- 是否可以使用分配器对象来释放另一个分配器分配的内存?
- 缓存局部性与函数调用
- 使用模板化分配器和对向量进行排序的函数
- C++ 中的分配器错误
- 堆栈与缓存友好分配器
- C++池分配器与静态分配,缓存性能