C ++:顺序访问元素时访问C-array的速度要快得多

时间:2016-03-17 22:55:12

标签: c++ arrays performance

我想在内存中存储一​​个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最快索引排序的卷,并提供相应的性能增益当相应地循环时。

注意:我的目标是在数据分配给卷且数据仍然正确读取的同时,无法在两种模式之间切换。

2 个答案:

答案 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像素的立方体)。因此,当您沿一个方向扫描时,您只会触摸其中一些页面,而当您沿另一个方向扫描时,您将触摸相同数量的不同页面。运气不错(取决于可用的物理内存),没有页面会不必要地移入和移出交换文件。

最好和最简单的解决方案是仅在一个方向上扫描像素。也许你真的不需要能够横向扫描像素。