我已经编写了一个用C ++编写的application,现在我必须深入到代码中并使其对缓存友好。
在阅读了presentation by Tony Albrecht之后,我很快意识到,只要在设计阶段直接应用这些原则,我就可以在第一时间做到正确。
Ulrich Drepper撰写的另一篇题为What Every Programmer Should Know About Memory的论文有一些优点,基本上告诉像我这样的开发人员要注意编写正确的内存布局以便缓存友好。
然而,感觉反直觉,因为:
一个很好的例子,当我坐下来写一个自定义分配器时,我将很快面对,有两个结构将由分配器处理,如下所示。
另请注意,一旦线程工作者释放一个元素,就必须使用相同的元素,因此它继续运行。
typedef struct
{
OVERLAPPED Overlapped;
WSABUF DataBuf;
CHAR Buffer[DATA_BUFSIZE];
byte *LPBuffer;
vector<byte> byteBuffer;
DWORD BytesSEND;
DWORD BytesRECV;
} PER_IO_OPERATION_DATA, *LPPER_IO_OPERATION_DATA;
typedef struct
{
SOCKET Socket;
} PER_HANDLE_DATA, *LPPER_HANDLE_DATA;
请注意,WSABUF.buf和vector可能是一个挑战,它们将如何布局在内存中。 WSABUF.buf和向量缓冲区分配是动态的,它不适合固定大小的连续布局。我想可能需要为这种情况创建一个单独的分配器。
PER_HANDLE_DATA是直截了当的,可以很容易地以连续的方式布局。
我必须设置另一个用于存储IsActive
的结构,以便它将在一个连续的块中布局,与PER_IO_OPERATION_DATA分开。
typedef struct {
bool isActive;
} IODATA_STAT, *LPIODATA_STAT
无论如何,我想获得一些反馈,说明为什么在编写应用程序后可以在启动时知道缓存?
另外,关于动态/固定缓冲区大小和指针重组数据的看法是什么?
答案 0 :(得分:3)
关于过早优化,我说如果您稍后可以在后见之明应用优化以响应分析器,而不会在代码库中进行一系列级联更改,那么它就为时过早。你必须在局部和非侵入性地交换表示的呼吸空间越大,你第一次担心这种表示最佳时就越少。
因此,您首先要关注的重点是界面设计,尤其是在您构建大型软件时。在适当的抽象级别建模的良好,稳定的界面将允许您分析代码并优化热点到smithereens而不会在整个代码中出现级联破坏:理想情况下只需对源文件进行一些调整。
生产力和可维护性仍然是开发人员最有价值的特征,绝大多数低于最低级别核心的代码库都将取决于这些特性远远超过您实现微效设计的能力,更不用说任务的最佳算法。编程世界现在已经非常饱和且竞争激烈,能够快速生成可维护应用程序的人通常是那些赢得并生存以优化另一天的人。
如果您没有使用分析器并担心除了广泛的算法复杂性之外的任何事情,那么您绝对需要首先使用分析器。测量两次,优化一次。分析器可以帮助您进行选择性的离散优化,这不仅仅是让您第一次以更有价值的方式花费时间,而且确保您不会降低您的性能。整个代码库成为维护的噩梦。
但抛开那个警告:
1。一般来说,对内存布局的思考并不自然。
在这里,我会推荐一些像C一样的想法。当你在职业生涯中遇到更多热点时,它会更自然地出现。在您的示例中,可变长度结构技巧变得非常有效。
struct PER_IO_OPERATION_DATA
{
...
byte byteBuffer[]; // size N
};
简单地使用PER_IO_OPERATION_DATA*
(或您自己的分配器)获取malloc
,结构大小+您需要使byteButter
侧足够大的额外N个字节。由于您使用C ++,您可以使用这种低级结构作为符合RAII的安全类后面的实现细节,在调试版本中应用必要的边界检查断言,具有异常安全性等等。在C ++中,尝试至少做到这一点:如果您需要在任何地方使用不安全的低级别位和字节操作代码,请将其作为隐藏在公共接口中的非常私有的实现细节。
这通常是内存局部性的第一次传递:使用堆来识别对象的运行时大小的聚合成员,并将它们与对象本身融合成一个连续的块。
当你尝试优化地方性(以及消除新的/删除/ malloc /免费热点)时,标准中缺少的另一种有用类型的通用容器类似于std :: vector与一个静态知道&#34;常见案例&#34;尺寸。基本示例:
struct Usually32ElementsOrLess
{
char buf[32];
char* ptr;
int num_elements;
};
初始化结构以使ptr
指向buf
,除非元素数超过固定大小(32)。在极少数情况下,make ptr
指向堆分配的动态数组。通过ptr
访问结构,而不是buf
,并确保实现正确的复制构造函数。
使用C ++,如果你喜欢使用模板参数来确定固定大小,你可以将它变成一个通用的STL兼容容器,如果你引入一个成员来跟踪当前的内存容量,甚至可以用push_backs调整大小除了尺寸。
拥有这种结构,经过充分测试,特别是在成熟的通用STL形式中,将真正帮助您更多地利用堆栈,并从更多日常代码中获取更多内存位置,而无需任何其他内容比使用std::vector
更耗时或更有风险。它适用于大多数情况下,数据大小在常见情况下具有上限,其中堆被保留用于那些罕见的特殊情况。
2。按照集合和行布置代码和数据并不自然。
实际上,在组织聚合和访问模式以协调和适应缓存行方面,这是非常不自然的。我建议你只为最关键的关键热点保存这样的想法。
3。根据具有属性和动作的对象进行思考是很自然的。
这并不妨碍其他两件事。这种公共界面设计,再次理想的公共界面,并没有使用该界面将这些低级优化细节泄露到客户端(除非它只是一个用作低级数据结构的低级数据结构)高级设计的构建块。)
回到界面设计,如果你想在不破坏界面设计的情况下为表现的高效优化留出更多空间,那么跨界设计将有很大帮助。查看OpenGL API以及它如何支持各种传递表示形式的各种方法。例如,它没有假设顶点位置存储在与顶点法线分开的连续存储块中。因为它在设计中使用了步幅,所以顶点法线可以与顶点位置交错,或者它们可以不是。它并不重要,并且不需要更改界面,因此它可以在不破坏任何内容的情况下试验内存布局。
在C ++中,您甚至可以像StrideIterator<T>(ptr, stride_size)
一样创建,以便更容易传递内容并在设计中返回它们,这些设计可以从传递和返回的内容布局的更改中受益。
由于您对自定义分配器感兴趣,请尝试使用以下尺寸:
#include <iostream>
#include <cassert>
#include <ctime>
using namespace std;
class Pool
{
public:
Pool(int element_size, int num_reserve)
{
if (sizeof(Chunk) > element_size)
element_size = sizeof(Chunk);
// This should use an aligned malloc.
mem = static_cast<char*>(malloc((num_reserve+1) * element_size));
char* ptr = static_cast<char*>(mem);
free_chunk = reinterpret_cast<Chunk*>(ptr);
free_chunk->next = 0;
Chunk* last_chunk = free_chunk;
for (int j=1; j < num_reserve+1; ++j)
{
ptr += element_size;
Chunk* chunk = reinterpret_cast<Chunk*>(ptr);
chunk->next = 0;
last_chunk->next = chunk;
last_chunk = chunk;
}
}
~Pool()
{
// This should use an aligned free.
free(mem);
}
void* allocate()
{
assert(free_chunk && free_chunk->next && "Reserve memory exhausted!");
Chunk* chunk = free_chunk;
free_chunk = free_chunk->next;
return chunk->mem;
}
void deallocate(void* mem)
{
Chunk* chunk = static_cast<Chunk*>(mem);
chunk->next = free_chunk;
free_chunk = chunk;
}
template <class T>
T* create(const T& other)
{
return new(allocate()) T(other);
}
template <class T>
void destroy(T* mem)
{
mem->~T();
deallocate(mem);
}
private:
union Chunk
{
Chunk* next;
// This should be max aligned.
char mem[1];
};
char* mem;
Chunk* free_chunk;
};
static double sys_time()
{
return static_cast<double>(clock()) / CLOCKS_PER_SEC;
}
int main()
{
enum {num = 20000000};
Pool alloc(sizeof(int), num);
// 'Touch' the array to reduce bias in the testing.
int** elements = new int*[num];
for (int j=0; j < num; ++j)
elements[j] = 0;
for (int k=0; k < 5; ++k)
{
// new/delete (malloc/free)
{
double start_time = sys_time();
for (int j=0; j < num; ++j)
elements[j] = new int(j);
for (int j=0; j < num; ++j)
delete elements[j];
cout << (sys_time() - start_time) << " seconds for new/delete" << endl;
}
// Branchless Fixed Alloc
{
double start_time = sys_time();
for (int j=0; j < num; ++j)
elements[j] = alloc.create(j);
for (int j=0; j < num; ++j)
alloc.destroy(elements[j]);
cout << (sys_time() - start_time) << " seconds for branchless alloc" << endl;
}
cout << endl;
}
delete[] elements;
}
我的机器上的结果:
1.711 seconds for new/delete
0.066 seconds for branchless alloc
1.681 seconds for new/delete
0.058 seconds for branchless alloc
1.668 seconds for new/delete
0.06 seconds for branchless alloc
1.68 seconds for new/delete
0.057 seconds for branchless alloc
1.663 seconds for new/delete
0.065 seconds for branchless alloc
它是一个无分支池分配器。不安全,但疯狂快。它要求您提前预留最大内存量,因此它最好用作分配器的构建块,分配器可以动态分支并创建多个这些预留池。