前几天我开始处理缓存友好代码并推出一些不同的构造来确定性能如何变化,如果我将变量放在堆栈或堆上,以及不同的内存布局如何与迭代和搜索等线性任务进行扩展。
我没有处理分配时间,只是处理性能。
测试不准确,但至少应该给出一些相关数字,表现可能会有所不同。
首先,我将std :: array与vector的性能进行了比较。
数组的测试代码:
@ftp_ssl_connect
常规矢量的代码:
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;
}
数组上的for_each花费了0.003到0.004秒,向量上的for_each花费了0.005到0.007秒。
在第一次测试之后,我推出了一个非常纤薄和简约的分配器,试试我是否可以获得与堆栈内存类似的性能。
分配器看起来像这样:
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;
}
这只是一个非常简约的测试样本,它不适合高效使用!
这是我的分配器测试样本:
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;
};
在这个例子中,for_each的性能介于0.003和0.004之间,就像第一个数组示例一样。
我知道,这些例子都没有清理。
所以这里有一个问题:因为我不得不增加visual studio 2015中的堆栈大小以使这个样本运行(否则会发生堆栈溢出)以及随着大小增加堆栈会变慢的简单事实是缓存友好代码的首选方法吗?
使用缓存友好的分配器使对象在堆上保持一致是达到与使用堆栈相同的性能(这可能在不同的示例中有所不同,但即使接近堆栈性能,对于我认为的大多数程序也是如此)。
构建一个合适的分配器并将大量内容存储在堆上并保持“真实”分配的数量低而不是过度使用堆栈会不会更有效?我之所以这样问是因为我在网上经常阅读“尽可能频繁地使用堆栈”,我担心这种方法并不像很多人想的那么简单。
谢谢。
答案 0 :(得分:3)
不要高估保持堆栈中所有内容的缓存值。是的,新分配的对象适合已经缓存的行很好。但是例如Haswell,缓存行只有64个字节,因此就缓存而言,你很快就会耗尽连续性。 (缓存集分布有一些好处,但它是次要的。)如果你正在编写那种实际上可以从额外的缓存一致性中受益的代码,那么你通常使用的是大型数组。无论在哪里,它们都是连续的。
我认为“使用堆栈而不是堆”的建议是建议你避免间接。
尽管如此, 对单独的分配器有一些小的好处,这个分配器假设并受益于LIFO分配模式。但它来自减少的簿记成本,而不是缓存友好性。