我如何解决指针数组中的数据依赖关系?

问题描述 投票:3回答:1

如果我们有一个整数指针数组,它们全部指向同一个int,并在其上进行++操作,则它比那些指向两个不同int的指针要慢100%。这是一个具体的例子

int* data[2];
int a, b;
a = b = 0;
for (auto i = 0ul; i < 2; ++i) {
    // Case 3: 2.5 sec
    data[i] = &a;

    // Case 2: 1.25 sec
    // if (i & 1)
    //     data[i] = &a;
    // else
    //     data[i] = &b;
}

for (auto i = 0ul; i < 1000000000; ++i) {
    // Case 1: 0.5sec
    // asm volatile("" : "+g"(i)); // deoptimize
    // ++*data[0];

    ++*data[i & 1];
}

总而言之,观察结果是:(描述了循环体)

情况1(快速):++ *指针[0]

情况2(中):++ * pointer [i],一半指针指向一个int,另一半指针指向另一int。

情况3(慢):++ * pointer [i],所有指针都指向同一int

这是我目前的想法。情况1很快,因为现代CPU知道我们在相同的内存位置进行读/写操作,从而缓冲了操作,而在情况2和情况3中,我们需要在每次迭代中写出结果。情况3比情况2慢的原因是,当我们通过指针a写入存储位置,然后尝试通过指针b读取它时,我们必须等待写入完成。这将停止超标量执行。

我理解正确吗?有没有什么方法可以使Case 3更快而不更改指针数组? (也许添加一些CPU提示?)

问题是从实际问题https://github.com/ClickHouse/ClickHouse/pull/7550中提取的

c++ performance compiler-optimization micro-optimization
1个回答
1
投票

您已经发现了导致直方图瓶颈的一种影响。该问题的一种解决方法是保留多个计数器数组并循环通过它们,因此相同索引的重复运行将分布在内存中的2个或4个不同的计数器上。

(然后在计数器数组上循环以将它们累加为一组最终的计数。这部分可以从SIMD中受益。)


情况1很快,因为现代CPU知道我们在相同的内存位置进行读/写操作,因此可以缓冲操作

不,不是CPU,它是编译时优化。

++*pointer[0]之所以快,是因为编译器可以将存储/重装提升到循环之外,而实际上只是增加一个寄存器。 (如果您不使用结果,它甚至可以优化结果。)

没有数据争用UB的假设使编译器假定没有其他事情正在修改pointer[0],因此,肯定是同一对象每次都递增。并且as-if规则允许它将*pointer[0]保留在寄存器中,而不是实际执行内存目标增量。

所以这意味着增量需要1个周期的延迟,当然,如果完全展开并优化了循环,它可以将多个增量合并为一个,并执行*pointer[0] += n


当我们通过指针a写入存储位置,然后尝试通过指针b读取它时,我们必须等待写入完成。这将停止超标量执行。

是的,通过该内存位置的数据依赖性是问题。在编译时不知道所有指针都指向同一位置的情况下,编译器将使asm确实增加指向的内存位置。

不过,“等待写入完成”并非严格准确。 CPU具有一个存储缓冲区,以使存储执行与高速缓存未命中脱钩,并从实际提交给L1d且对其他内核可见的存储中乱序的推测执行。重新加载最近存储的数据不必等待提交缓存。一旦CPU检测到,从存储缓冲区到重新加载的存储转发就是一件事情。

在现代Intel CPU上,存储转发延迟约为5个周期,因此内存目标添加具有6个周期的延迟。 (如果在关键路径上,则1用于添加,5用于存储/重新加载。)

并且是的,无序执行使这些6周期延迟依赖关系链中的两个并行运行。而且,OoO执行人员再次将循环开销隐藏在该延迟下。

相关:

是的,如果是那样的话,也许在它上面的[[branch

int *current_pointer = pointer[0]; int repeats = 1; ... loop { if (pointer[i] == current_pointer) { repeats++; } else { *current_pointer += repeats; current_pointer = pointer[i]; repeats = 1; } } 我们通过
计算重复相同指针的游程长度]进行优化。

这完全被案例2击败了],如果

not

常见,则性能会很差。
短跑可以被乱序执行程序隐藏;只有当dep链变得足够长以填满ROB(重排序缓冲区)时,我们才真正停止运行。
© www.soinside.com 2019 - 2024. All rights reserved.