加快对图形中邻居的迭代
Speed up Iteration Over Neighbors in a Graph
我有一个静态图(拓扑不会随时间变化,并且在编译时是已知的),其中图中的每个节点都可以具有三种状态之一。然后,我模拟了一个动态,其中节点有随时间改变其状态的概率,而这个概率取决于其邻居的状态。随着图形变大,模拟开始变得非常慢,但是经过一些分析后,我发现大部分计算时间都花在迭代邻居列表上。
我能够通过更改用于访问图中邻居的数据结构来提高模拟的速度,但想知道是否有更好(更快)的方法可以做到这一点。 我目前的实现是这样的:
对于具有从0
到N-1
标记的N
个节点和K
的平均邻居数的图,我将每个状态存储为std::vector<int> states
中的整数,并将每个节点的邻居数存储在std::vector<int> number_of_neighbors
中。
为了存储邻居信息,我又创建了两个向量:std::vector<int> neighbor_lists
向量,它按顺序存储节点0
的邻居节点,节点1
,...,节点N
,以及一个索引向量std::vector<int> index
,它为每个节点存储其第一个邻居的索引neighbor_lists
。
所以我总共有四个向量:
printf( states.size() ); // N
printf( number_of_neighbors.size() ); // N
printf( neighbor_lists.size() ); // N * k
printf( index.size() ); // N
更新节点i
时,我像这样访问其邻居:
// access neighbors of node i:
for ( int s=0; s<number_of_neighbors[i]; s++ ) {
int neighbor_node = neighbor_lists[index[i] + s];
int state_of_neighbor = states[neighbor_node];
// use neighbor state for stuff...
}
总结一下我的问题:在固定的图结构中访问相邻节点是否有更快的实现?
目前,我已经在相当多的模拟时间内达到了 N = 5000,但如果可能的话,我的目标是 N ~ 15.000。
知道N
的数量级很重要,因为,如果它不高,你可以利用你知道编译时拓扑的事实,这样你就可以将数据放在已知维度的std::array
s(而不是std::vector
s),使用尽可能小的类型来(如有必要)节省堆栈内存, AD 将其中一些定义为constexpr
(除states
外的所有
)。因此,如果N
不是太大(堆栈限制!),您可以定义
states
作为std::array<std::uint_fast8_t, N>
(8 位 3 状态就足够了)number_of_neighbors
作为constexpr std::array<std::uint_fast8_t, N>
(如果最大邻居数小于 256,否则为更大的类型)neighbor_list
作为constexpr std::array<std::uint_fast16_t, M>
(其中M
是邻居数的已知总和),如果 16 位足以N
;否则为更大的类型index
作为constexpr std::array<std::uint_fast16_t, N>
,如果 16 位足以M
;否则更大的类型
我认为(我希望)使用已知维度的数组constexpr
(如果可能)编译器可以创建最快的代码。
关于更新代码...我是一个老 C 程序员,所以我习惯于尝试以现代编译器做得更好的方式优化代码,所以我不知道以下代码是否是一个好主意;无论如何,我会像这样编写代码
auto first = index[i];
auto top = first + number_of_neighbors[i];
for ( auto s = first ; s < top ; ++s ) {
auto neighbor_node = neighbor_lists[s];
auto state_of_neighbor = states[neighbor_node];
// use neighbor state for stuff...
}
--编辑--
OP 指定
目前,我已经在相当多的模拟时间内达到了 N = 5000,但如果可能的话,我的目标是 N ~ 15.000。
所以 16 位应该足够了 - 对于neighbor_list
和index
的类型 - 并且
states
和number_of_neighbors
各约为 15 kB(使用 16 位变量时为 30 kB)index
约为 30 kB。
在我看来,这是堆栈变量的合理值。
问题可能是neighbor_list
;如果邻居的中等数量很低,比如说10来固定一个数字,我们有M
(邻居的总和)大约是150'000,所以neighbor_list
大约是300 kB;不低,但对于某些环境来说是合理的。
如果中值很高(例如 100),则修复另一个数字--,neighbor_list
变为大约 3 MB;在某些环境中,它应该是高。
目前,您正在访问每次迭代的 sum(K) 节点。这听起来还不错...直到您点击访问缓存。
对于少于 2^16 个节点,您只需要一个uint16_t
来标识节点,但对于 K 个邻居,您将需要一个uint32_t
来索引邻居列表。 如前所述,这 3 种状态可以存储在 2 位中。
所以有
// your nodes neighbours, N elements, 16K*4 bytes=64KB
// really the start of the next nodes neighbour as we start in zero.
std::vector<uint32_t> nbOffset;
// states of your nodes, N elements, 16K* 1 byte=16K
std::vector<uint8_t> states;
// list of all neighbour relations,
// sum(K) > 2^16, sum(K) elements, sum(K)*2 byte (E.g. for average K=16, 16K*2*16=512KB
std::vector<uint16_t> nbList;
您的代码:
// access neighbors of node i:
for ( int s=0; s<number_of_neighbors[i]; s++ ) {
int neighbor_node = neighbor_lists[index[i] + s];
int state_of_neighbor = states[neighbor_node];
// use neighbor state for stuff...
}
将代码重写为
uint32_t curNb = 0;
for (auto curOffset : nbOffset) {
for (; curNb < curOffset; curNb++) {
int neighbor_node = nbList[curNb]; // done away with one indirection.
int state_of_neighbor = states[neighbor_node];
// use neighbor state for stuff...
}
}
因此,要更新一个节点,您需要从states
读取当前状态,读取nbOffset
的偏移量,并使用该索引查找邻居列表nbList
和来自nbList
的索引以查找states
中的邻居状态。
如果您线性运行列表,前 2 个很可能已经在 L1$ 中。如果您线性计算节点,则从每个节点的nbList
读取第一个值可能为 L1$,否则它很可能导致 L1$ 和 L2$ 未命中,以下读取将是硬件预取的。
线性读取节点具有额外的优势,即每个相邻列表在节点集的每次迭代中仅读取一次,因此states
停留在 L1$ 中的可能性将大大增加。
减小states
的大小可以进一步提高它停留在L1$的机会,只需稍加计算,就可以在每个字节中存储4个2位的状态,从而将states
的大小减小到4KB。因此,根据您执行的"内容"量,缓存未命中率可能非常低。
但是,如果您在节点中跳来跳去并执行"操作",情况很快就会变得更糟,导致几乎保证nbList
的L2$未命中和当前节点的潜在L1$未命中以及state
的K调用。这可能导致减速 10 到 50 倍。
如果您在后一种情况下使用随机访问,您应该考虑在邻居列表中存储状态的额外副本,从而节省访问states
K 次的成本。你必须衡量这是否更快。
关于在程序中内联数据,您将因不必访问向量而获得一点收益,在这种情况下,我估计它从中获得的收益不到 1%。
内联和 constexpr 积极地将您的计算机煮沸多年,并回复"42"作为程序的最终结果。你必须找到一个中间立场。
- 使用std::multimap迭代器创建std::list
- 来自 std::list 的迭代器 .end() 按预期返回"0xcdcdcdcdcdcdcdcd"但 .begin()
- C++中带有List类的迭代器Segfault
- 迭代时从向量和内存中删除对象
- 如何在c++迭代器类型中包装std::chrono
- 带过滤器的现代迭代c++集合
- 在c++中检查长方体是否尽可能快地重叠(无迭代)
- C++矢量迭代
- 集合上的输出迭代器:assign和increment迭代器
- Boost Spirit,获取迭代器内部语义动作
- 擦除while循环中迭代的元素
- 实现一个在集合上迭代的模板函数
- 对于set上的循环-获取next元素迭代器
- 在向量内的向量上迭代
- 为什么output_editor Concept不需要output_e迭代器标记
- TSP递归解的迭代形式
- c++17文件系统::recursive_directory迭代器()在mac上没有给出这样的目录,但在windows上
- 使用迭代器时如何访问对象在向量中的位置?
- std::vector::迭代器是否可以合法地作为指针
- 加快对图形中邻居的迭代