我想在内存中存储一个3d卷。我为此目的使用线性数组,然后从3d索引计算1d-index。它包含在一个名为Volume
的类中,该类提供了访问数组数据元素的函数。以下是访问卷的一个数据元素的功能:
template<typename T>
inline T& Volume<T>::at(size_t x, size_t y, size_t z) {
if (x >= this->xMax || y >= this->yMax || z >= this->zMax) throw std::out_of_range("Volume index out of bounds");
return this->volume[x * this->yMax*this->zMax + y*this->zMax + z]
}
现在,这将使用Z最快的索引顺序对3d体积进行线性化。如果在这样的循环中访问卷,它将在卷元素上顺序迭代,因为它们位于内存中:
Volume<float> volume(10, 20, 30); //parameters define size
for(int x = 0; x < volume.xSize(); ++x) {
for(int y = 0; y < volume.ySize(); ++y) {
for int z = 0; z < volume.zSize(); ++z) {
volume.at(x, y, z); //do sth with this voxel
}
}
}
但是,如果我像这样编写循环,它们将不会按顺序访问,而是以更“随机”的顺序访问:
Volume<float> volume(10, 20, 30); //parameters define size
for(int z = 0; z < volume.zSize(); ++z) {
for(int y = 0; y < volume.ySize(); ++y) {
(for int x = 0; x < volume.zSize(); ++x) {
volume.at(x, y, z); //do sth with this voxel
}
}
}
现在,第一种情况运行得很快,第二种情况很慢。我的第一个问题是:为什么?我想它与缓存有关,但我不确定。
现在,我可以像这样重写卷元素的访问函数:
template<typename T>
inline T& Volume<T>::at(size_t x, size_t y, size_t z) {
if (x >= this->xMax || y >= this->yMax || z >= this->zMax) throw std::out_of_range("Volume index out of bounds");
return this->volume[x * this->yMax*this->zMax + y*this->zMax + z]
}
然后循环顺序#2会很快(因为访问顺序发生),但循环顺序#1变慢。
现在,出于某种原因,我需要在我的程序中使用两个索引顺序。两者都应该很快。我们的想法是,可以在创建卷时定义索引顺序,然后使用此索引排序。首先,我在at
函数中尝试了一个简单的if-else语句。然而,这似乎没有成功。
所以我在设置订购模式时尝试了类似的东西:
template<typename T>
void Volume<T>::setMemoryLayout(IndexOrder indexOrder) {
this->mode = indexOrder;
if (indexOrder == IndexOrder::X_FASTEST) {
this->accessVoxel = [this](size_t x, size_t y, size_t z)->T& {
return this->volume[z * this->yMax*this->xMax + y*this->xMax + x];
};
} else {
this->accessVoxel = [this](size_t x, size_t y, size_t z)->T& {
return this->volume[x * this->yMax* this->zMax + y*this->zMax + z];
};
}
}
然后当实际访问体素时:
template<typename T>
inline T& Volume<T>::at(size_t x, size_t y, size_t z) {
if (x >= this->xMax || y >= this->yMax || z >= this->zMax) throw std::out_of_range("Volume index out of bounds");
return this->accessVoxel(x, y, z);
}
所以我的想法是通过在当前模式改变时动态定义一次lambda函数来减少at
函数内部必需的if语句的开销。然后只需在调用at
时调用它。然而,这并没有实现我想要的目标。
我的问题是为什么我的尝试不起作用,如果有一种方法我可以实际做我想要的:一个支持X最快和Y最快索引排序的卷,并提供相应的性能增益当相应地循环时。
注意:我的目标是在数据分配给卷且数据仍然正确读取的同时,无法在两种模式之间切换。
答案 0 :(得分:3)
在我的cpu(可能是你的)上我有64字节的缓存行。每个缓存行包含16个4字节浮点数。当为第一个浮点数提取缓存行时,在顺序访问时,您不需要为后面的15重复该操作。
请注意,从主内存中获取缓存行大约需要240个周期。从L1缓存中获取就像是12个循环,如果你可以反复命中L1,这是一个很大的区别。 (L2成本约为40个周期,L3 150个周期)
顺序访问的第二个缓存胜利是CPU在顺序读取时会为您预取数据到缓存中。因此,如果您从数组的开头开始并按顺序移动它,您甚至可以避免读取缓存行的代价。
L1通常是32k的数据(和32k的指令缓存),对我来说这台机器上的L2是256K,L3是兆字节。因此,您可以保持较小的内存工作集,在给定的缓存中可以容纳的内存越多。在L1中拟合它是最佳的。
顺序访问最佳的第三个原因是它为编译器提供了向量化指令的机会。即使用SSE或AVX指令。 AVX寄存器是32个字节,因此可以容纳8个浮点数。您可以同时操作阵列中的8个连续项目,速度提高8倍。
答案 1 :(得分:0)
计算机的物理内存不足以容纳整个阵列。解决问题的一个方法是添加更多内存。
您的操作系统适用于虚拟内存。只要需要更多内存,虚拟内存页面就会移动到磁盘。访问磁盘非常耗时,而这正是导致性能下降的原因。在更糟糕的情况下,操作系统始终保持书写和阅读(或只是阅读)页面。因此,另一种解决方案是重新组织数据,使磁盘访问大致相同,无论像素扫描的方向如何。我建议让3D区域的大小与页面大小相同(通常为4KB,因此大小为16像素的立方体)。因此,当您沿一个方向扫描时,您只会触摸其中一些页面,而当您沿另一个方向扫描时,您将触摸相同数量的不同页面。运气不错(取决于可用的物理内存),没有页面会不必要地移入和移出交换文件。
最好和最简单的解决方案是仅在一个方向上扫描像素。也许你真的不需要能够横向扫描像素。