堆栈与缓存友好分配器

Stack vs cache friendly allocator

本文关键字:分配器 缓存 堆栈      更新时间:2023-10-16

几天前,我开始处理缓存友好的代码,并滚动了一些不同的结构来确定如果我将变量放在堆栈或堆上,性能如何变化,以及不同的内存布局如何与线性任务(如迭代和搜索)一起扩展。

我不是在处理分配时间,只是在处理性能。

测试不准确,但至少应给出一些相关的数字,性能可能有何不同。

首先,我比较了 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分配模式并从中受益的单独分配器有一些小的好处。但它来自降低的簿记成本,而不是缓存友好性。