14

我编写了以下简短的 C++ 程序来重现Herb Sutter描述的虚假共享效果:

比如说,我们想要执行全部的 WORKLOAD 整数操作,并且我们希望它们平均分配到一定数量 (PARALLEL) 的线程中。出于此测试的目的,每个线程将从整数数组中增加自己的专用变量,因此该过程可能是理想的可并行化的。

void thread_func(int* ptr)
{
    for (unsigned i = 0; i < WORKLOAD / PARALLEL; ++i)
    {
        (*ptr)++;
    }
}

int main()
{
    int arr[PARALLEL * PADDING];
    thread threads[PARALLEL];

    for (unsigned i = 0; i < PARALLEL; ++i)
    {
        threads[i] = thread(thread_func, &(arr[i * PADDING]));
    }
    for (auto& th : threads)
    {
        th.join();
    }
    return 0;
}

我认为这个想法很容易掌握。如果你设置

#define PADDING 16

每个线程都将在单独的缓存行上工作(假设缓存行的长度为 64 字节)。所以结果将是加速的线性增加,直到 PARALLEL > # 个核心。另一方面,如果 PADDING 设置为低于 16 的任何值,则应该会遇到严重的争用,因为现在至少有两个线程可能在同一个缓存行上运行,但是受到内置硬件互斥锁的保护。我们希望我们的加速在这种情况下不仅是亚线性的,而且甚至总是 < 1,因为不可见的锁护卫队。

现在,我的第一次尝试几乎满足了这些期望,但避免虚假共享所需的最小 PADDING 值大约是 8 而不是 16。我困惑了大约半个小时,直到我得出一个明显的结论,即无法保证让我的数组与主内存中缓存行的开头完全对齐。实际对齐可能会因许多条件而异,包括数组的大小。

在这个例子中,我们当然不需要以特殊的方式对齐数组,因为我们可以将 PADDING 保留为 16,一切正常。但是人们可以想象这样的情况,它确实会产生影响,某个结构是否与高速缓存行对齐。因此,我添加了一些代码行来获取有关数组实际对齐的一些信息。

int main()
{
    int arr[PARALLEL * 16];
    thread threads[PARALLEL];
    int offset = 0;

    while (reinterpret_cast<int>(&arr[offset]) % 64) ++offset;
    for (unsigned i = 0; i < PARALLEL; ++i)
    {
        threads[i] = thread(thread_func, &(arr[i * 16 + offset]));
    }
    for (auto& th : threads)
    {
        th.join();
    }
    return 0;
}

尽管在这种情况下这个解决方案对我来说效果很好,但我不确定这是否是一个好的方法。所以这是我的问题:

除了我在上面的示例中所做的之外,还有什么常见的方法可以让内存中的对象与缓存行对齐?

(使用 g++ MinGW Win32 x86 v.4.8.1 posix dwarf rev3)

4

3 回答 3

15

您应该能够从编译器请求所需的对齐:

alignas(64) int arr[PARALELL * PADDING]; // align the array to a 64 byte line
于 2013-08-14T16:05:15.640 回答
4

gcc 支持对齐的关键字: http: //gcc.gnu.org/onlinedocs/gcc/Variable-Attributes.html

你可能想要这样的东西:

int arr[PARALLEL * 16] __attribute__ ((aligned (8)));

这与arr八字节边界对齐。

Visual Studio 也有类似的功能:http: //msdn.microsoft.com/en-us/library/83ythb65.aspx

于 2013-08-14T16:05:24.643 回答
3

在现代 C++(17 及更高版本)中,您应该使用hardware_constructive_interference_size

于 2020-02-12T19:02:09.257 回答