【问题标题】:parallel push_back for vector of vector向量的向量的并行 push_back
【发布时间】:2021-10-29 03:26:03
【问题描述】:

我有一个大约 13G 的大文本文件,其内容是图形的边缘列表。每行有两个整数uv代表边的端点。我想把它读成向量的向量作为图的邻接向量。

然后是下面的代码。

const int N = 3328557;
vector<vector<int> >adj{N};

int main() {
    FILE * pFile;
    pFile = fopen("path/to/edge/list", "r");

    int u, v;
    while (fscanf(pFile, "%d%d", &u, &v) == 2) {
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    
    fclose(pFile);
}

大约需要 7 分钟。经过一些分析,我发现adj[u].push_back(v)adj[v].push_back(u)由于地址随机而消耗的时间最多。

然后我使用一个二维数组作为缓存。填满后,我将所有值复制到向量并清除它。

const int N = 3328557;
const int threshold = 100;

vector<vector<int> >adj{N};
int ln[N];
int cache[N][threshold];

void write2vec(int node) {
    for (int i = 0; i < ln[node]; i++)
        adj[node].push_back(cache[node][i]);
    ln[node] = 0;
}

int main() {
    FILE * pFile;
    pFile = fopen("path/to/edge/list", "r");

    int u, v;
    while (fscanf(pFile, "%d%d", &u, &v) == 2) {
        cache[u][ln[u]++] = v;
        if (ln[u] == threshold)
            write2vec(u);
        cache[v][ln[v]++] = u;
        if (ln[v] == threshold)
            write2vec(v);
    }

    for (int i = 1; i < N; i++)
        write2vec(i);
    
    fclose(pFile);
}

这次耗时 5.5 分钟。还是太长了。那么我认为第一个代码中的两个push_back可以并行化。但我不知道该怎么做。还有没有人有其他想法?

谢谢。

编辑。

我认为我的第二种方法更快的原因是对向量的向量寻址更慢。向量的向量地址不连续,所以访问adj[u]需要两个操作,先是adj,然后是adj[u]

所以我想知道是否可以使用多处理来使寻址并行化。

【问题讨论】:

  • 我怀疑并行化 push_back() 调用会改变很多,因为 fscanf() 调用隐式同步每个循环迭代,并且 I/O 可能对相当多的经过时间负责。您可能想要估计(上限)adj[u]adj[v] 的大小,并且 - 使用向量 - 相应地保留,而不是依赖于可能在每次循环迭代中重新分配/调整大小。
  • @Peter I/O 消耗大约 2.5 分钟。而且我不认为调整矢量的大小是瓶颈。因为我已经测试了push_back 到向量的向量随机消耗的时间是push_back 排序的十倍。
  • 13G 足够大,您可能会开始考虑其他选择。但你需要退后一步思考。显然你不止一次地解析这个文件,因为你可以轻松地花费超过 7 分钟来优化它。总是一样的13G吗?在这种情况下,对它进行预处理可能是明智的。例如。标准化使得 uv 并在完成加载后创建反向映射 v->u。
  • 一个alternative approach。我不知道它的表现如何。你必须在你的文件上测试它。
  • @MSalters 但是创建反向映射也是随机访问。

标签: c++ c++11 vector


【解决方案1】:

“我认为第一个代码中的两个push_back可以并行化。”

您的 CPU 可能会同意。鉴于数据大小,这很可能会遇到从 L3 缓存到主存的瓶颈。现代 CPU 内核能够乱序执行,这看起来 CPU 会很高兴地从属于第二个 push_back 的指令开始,而第一个正在等待主内存。这正是乱序执行成为常见功能的原因。

主要问题是重新分配 - 您没有预留容量。并且重新分配不是简单的 CPU 操作;它需要访问全局堆。我建议为每个内部向量保留 128/sizeof(int) 元素。这是普通 CPU 上的一两个缓存线,因此您没有向量共享缓存线。

【讨论】:

  • 我认为瓶颈正在解决,因为我的第二种方法更快。我认为parallel push_back可以并行寻址,所以也许更快。
  • @HuZhang:寻址? x86 上的地址生成基本上是免费的(流水线)。访问主内存是缓慢的部分,但这不是您可以并行化的。
  • 但为什么第二种方法更快?
  • @HuZhang:有缓存行大小的预留?这意味着两个 push_back 将在不同的缓存行上运行。这意味着 read-modify-write 周期独立运行。
猜你喜欢
  • 2013-03-28
  • 1970-01-01
  • 2021-06-23
  • 2020-11-20
  • 2021-10-12
  • 2017-03-24
  • 2016-11-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多