优化
关于过早的优化,我想说,如果您事后可以应用优化来响应分析器而不在您的代码库中进行一系列级联更改,那么现在还为时过早。您必须在本地和非侵入性地相当地交换表示的喘息空间越大,您第一次就不必担心如何使该表示达到最佳状态。
因此,您首先要关注的关键是界面设计,而不是实施,尤其是在您构建大型软件时。在适当的抽象级别建模的良好、稳定的接口将允许您分析代码并优化热点,而不会在整个代码中级联损坏:理想情况下,只需对源文件进行一些调整。
生产力和可维护性仍然是开发人员最有价值的特征,除了最低级别的核心之外,绝大多数代码库都将依赖于这些特征,而不是您实现微高效设计的能力,更不用说任务的最佳算法。编程世界现在非常饱和且竞争激烈,能够快速生产出可维护应用程序的人通常是赢得并生存下来以优化另一天的人。
如果您不使用分析器并且担心的不仅仅是广泛的算法复杂性,那么您首先绝对需要一个分析器。测量两次,优化一次。分析器可以让您进行选择性的、离散的优化,这不仅可以让您在第一次以更有价值的方式花费您的时间,而且可以确保您不会将整个代码库降级为维护的噩梦.
内存布局
但除此之外:
1.一般来说,考虑内存布局并不自然。
在这里,我会推荐一些类似 C 的思维方式。当您在职业生涯中面临更多热点时,它会变得更加自然。在您的示例中,可变长度结构技巧变得非常有效。
struct PER_IO_OPERATION_DATA
{
...
byte byteBuffer[]; // size N
};
只需使用 malloc(或您自己的分配器)和结构大小 + 使 byteButter 侧足够大所需的额外 N 字节来获取 PER_IO_OPERATION_DATA*。由于您掌握了 C++,因此您可以使用这种低级结构作为符合 RAII 的安全类背后的实现细节,在调试构建中应用必要的边界检查断言,以及异常安全等等。在 C++ 中,至少尝试这样做:如果您在任何地方需要不安全的低级位和字节操作代码,请将其设置为对公共接口隐藏的非常私密的实现细节。
这通常是内存局部性的第一遍:使用堆识别对象的运行时大小的聚合成员,并将它们与对象本身融合到一个连续的块中。
当您尝试针对局部性进行优化(以及消除新/删除/malloc/free 热点)时,标准中缺少另一种有用的通用容器类型,例如 std::vector 具有静态已知的“常见案例”大小。基本示例:
struct Usually32ElementsOrLess
{
char buf[32];
char* ptr;
int num_elements;
};
初始化结构以使ptr 指向buf,除非元素数量超过固定大小(32)。在这种罕见的情况下,使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
这是一个无分支池分配器。不安全,但快疯了。它要求您提前预留最大内存量,因此最好将其用作分配器的构建块,该分配器执行分支并动态创建多个这些预留池。