【问题标题】:How can I accelerate this code (MWE!), e.g. using restrict我怎样才能加速这个代码(MWE!),例如使用限制
【发布时间】:2019-01-06 09:04:01
【问题描述】:

有什么办法可以加速这个功能:

void task(int I, int J, int K, int *L, int **ids, double *bar){ 
    double *foo[K];
    for (int k=0;k<K;k++)
        foo[k] = new double[I*L[k]];
        // I am filling these arrays somehow
        // This is not a bottleneck, hence omitted here        
    for (int i=0;i<I;i++)
        for (int j=0;j<J;j++){
            double tmp = 1.;
            for (int k=0;k<K;k++)
                tmp *= foo[k][i*L[k]+ids[j][k]]; //ids[j][k]<L[k]
            bar[i*J+j] = tmp;
        }
}

典型值为:I = 100,000J = 10,000K=3L=[50,20,60]

我读到__restrict__ 关键字/扩展名可能会有所帮助,但我不确定如何在此处应用它。例如,尝试将其放入foo[k] = new double[...] 的定义中,我得到error: '__restrict_ qualifiers cannot be applied to double。此外,我不知道我是否应该/如何将idsids[j], 1&lt;= j&lt;= J 声明为受限。

请注意,在我的实际代码中,我会在与我的 CPU 内核一样多的线程中并行执行此类任务。

我主要编写与 C 兼容的 C++,因此欢迎使用两种语言的解决方案。

【问题讨论】:

  • restrict 不是 C++ 的一部分,也没有明确定义。有关详细信息,请参阅stackoverflow.com/questions/776283/…。填写std::vector 并按值返回怎么样,甚至不需要限制
  • 这里不需要restrict,你不要修改可能的别名。
  • 您是否使用这种不规则/嵌套的数组格式,而不是平面的一维数组?除了__restrict 之外,可能还有加速,但我不确定我是否理解这段代码在做什么。可能只是手动将ids[j] 之类的东西从循环中提升出来会有所帮助,而循环foo[k][ stuff(k) ] 对于内部循环来说看起来很讨厌。你不能在你的初始循环中构建一个更有用的数据结构,在三重嵌套循环之外,给你更好的内存访问模式吗?
  • double *foo[K]; 不是有效的 C++,因为它使用 VLA 扩展。
  • @Bananach 如果您有工作代码并要求改进,您最好将这个问题发布到SE Code Review。确保您的问题符合那里的政策设置。

标签: c++ c optimization compiler-optimization micro-optimization


【解决方案1】:

https://en.cppreference.com/w/c/language/restrict 声称你可以在 C99/C11 中声明一个 restrict 指针数组,像这样在 C99/C11 中加倍:

typedef double *array_t[10];
restrict array_t foo;        // the type of a is double *restrict[10]

但只有 gcc 接受。我认为这是一个 GCC 主义,而不是有效的 ISO C11。 (gcc 也接受
array_t restrict foo_r;,但没有其他编译器接受。)

ICC 警告 "restrict" is not allowed,clang 拒绝它

<source>:16:5: error: restrict requires a pointer or reference ('array_t' (aka 'double *[10]') is invalid)
    restrict array_t foo_r;
    ^

MSVC 用error C2219: syntax error: type qualifier must be after '*' 拒绝它

我们从这些带有__restrict 的编译器中获得了基本相同的C++ 行为,它们接受为具有与C99 restrict 相同语义的C++ 扩展。


作为一种解决方法,您可以在每次从 foo 读取时使用合格的临时指针,而不是 f[k][stuff] 我认为这可以保证您通过 fk 引用的内存与您通过声明 fk 的块中的任何其他指针访问的内存不同。

double *__restrict fk = foo[k];
tmp *= fk[ stuff ];

我不知道如何向编译器保证f[0..K-1] 指针之间没有一个别名。我不认为这可以实现。


这里不需要 __restrict。

根据 Godbolt 编译器资源管理器上的差异窗格:https://godbolt.org/z/4YjlDA,我将 __restrict 添加到所有指针声明中,例如 int *__restrict *__restrict ids 并且它根本不会更改 asm。 正如我们所期望的那样,因为基于类型的别名让编译器假设 double 存储到 bar[] 不会修改 int *ids[] 的任何 int * 元素。 正如人们所说在 cmets 中,这里没有编译器无法解决的别名。在实践中,它似乎确实对其进行了排序,没有任何额外的指针重新加载。

它也不能别名*foo[k],因为我们在这个函数中得到了带有new的指针。他们不能指向内部bar[]

(所有主要的 x86 C++ 编译器(GCC、clang、ICC、MSVC)都支持 C++ 中的 __restrict,其行为与 C99 restrict 相同:对通过此指针存储的编译器的承诺不会修改对象由另一个指针指向的。 我会推荐__restrict 而不是__restrict__,至少如果您主要希望跨x86 编译器具有可移植性的话。除此之外我不确定。)

您似乎是在说您试图将 __restrict__ 放入一个作业中,而不是一个声明中。那是行不通的,__restrict 应用的是指针变量本身,而不是对它的单个赋值。


问题的第一个版本在内部循环中有一个错误:它有 K++ 而不是 k++,所以这是纯粹的未定义行为,编译器变得很奇怪。 asm 没有任何意义(例如,没有 FP 乘法指令,即使 foo[] 是函数 arg 也是如此)。这就是为什么对数组维度使用 klen 之类的名称而不是 K 是个好主意。

在 Godbolt 链接上修复该问题后,在所有内容上使用/不使用 __restrict 的 asm 仍然没有区别,但它更加理智。

顺便说一句,将 double *foo[] 设为函数 arg 会让我们仅查看主循环的 asm。您实际上需要__restrict,因为bar[] 的存储可以修改foo[][] 的元素。这不会在您的函数中发生,因为编译器知道 new 内存没有被任何现有指针指向,但它不知道 foo 是否是函数 arg .


循环中的少量工作是在将 32 位 int 结果用作具有 64 位指针的数组索引之前对它们进行符号扩展。这会在某处增加一个延迟周期,但不会增加循环承载的 FP 乘法依赖链,因此它可能无关紧要。您可以通过使用size_t k=0; 作为最内层循环计数器来摆脱 x86-64 上的内循环内的一条指令。 L[] 是一个 32 位数组,因此 i*L[k] 需要在循环内进行符号扩展。从 32 位到 64 位的零扩展在 x86-64 上是免费的,因此 i * (unsigned)L[k] 在指针追踪 dep 链中保存了 movsx 指令。然后 gcc8.2 制作的内部循环是您讨厌的数据结构/布局所需的所有必要工作。 https://godbolt.org/z/bzVSZ7

我不知道这是否会有所作为。我认为导致缓存未命中的内存访问模式更有可能成为您处理真实数据的瓶颈。

它也不能自动矢量化,因为数据不连续。但是,您无法通过循环 ji 获得连续的源数据。至少i 将是一个简单的进步,而不必重做ids[j][k]

如果您生成 foo[k][...]bar[...] 转置,因此您使用 foo[k][ i + L[k] * ids[j][k] ] 进行索引,那么您将在 src 和 dst 中拥有连续的内存,因此您(或编译器)可以使用 SIMD 乘法。

【讨论】:

  • 我很抱歉这个错误。将代码粘贴到此处后,我不应该开始编辑代码
  • 我确实尝试过foo[k] = new __restrict__ double[I*L[k]];。我明白你的观点,__restrict 应该是声明而不是定义的一部分,但问题是:如何声明一个指针数组,使得每个条目的类型为 double *__restrict
  • 谢谢,我才明白为什么 bar 和 foo 没有混叠危险。
  • 我是否理解正确,因为使用 32 位整数索引 64 位指针(例如 bar)很昂贵,我应该将 ij 设为 64 位?此外,如果我理解正确,我应该使用i*(unsigned)L[k] 而不是i*L[k]size_t k 而不是int k。但是uint64 Luint64_t k 不是更好吗?
  • @Bananach:哦,声明double *__restrict foo[10]; 是一个有趣的问题。该语法不起作用,但似乎有一种使用 gcc 接受的 typedef 的方法。 godbolt.org/z/0JJJSv。但是其他编译器拒绝它,所以它可能是 GCC 主义而不是有效的 ISO C11。我可能会问一个新问题,因为这个问题主要是问__restrict 是否会有所作为。当然,您可以在每次从数组加载时使用double *__restrict fk = foo[k]; 来解决任何编译器问题。
【解决方案2】:

restrict 在这种情况下无关紧要。

您的算法很垃圾,并且不允许使用长向量运算(因此微优化在这里根本没有帮助)。

你需要找到内循环中的元素占用数组索引的连续块的方式。正如现在所做的那样,编译器必须从数组中的不同位置读取每个元素,它使编译器免受循环展开和更长的向量指令的影响。它也可能对缓存非常不友好。

首先重新考虑算法 - 如果算法效率极低,过早的优化将无济于事

编辑

在 OP 评论之后,我只想向他展示“天真”和更高效(不那么天真但更难理解)之间的区别

让我们考虑 32 位无符号值的奇偶校验。天真的方法:

int very_naive_parity(const uint32_t val)
{
    unsigned parity = 0;
    
    for(unsigned bit = 0; bit < 32; bit++)
    {
        if(val & (1U << bit))
        {
            parity = !parity;
        }
    }
    return parity;
}

它很容易编写和理解,但效率极低。至少要执行 288 条指令来计算这个奇偶校验。

更高效:

int parity(const uint32_t val)
{
    uint32_t tmp = val;
    
    tmp ^= tmp >> 16;
    tmp ^= tmp >> 8;
    tmp ^= tmp >> 4;
    return (0b110100110010110 >> (tmp & 0x0f)) & 1;
}

将在 9 条指令中执行(没有函数序言和结语的计算) 是不是更难理解? - 绝对是的。但正如我所写,效率通常对人类来说意味着不那么容易。

【讨论】:

  • “你的算法很垃圾”。没有任何改进它的建议,这可能有点苛刻。对于ids 的任意输入,您知道有什么方法可以给出与task 完全相同的结果吗?
  • @Bananach 我在回答中指明了方向。您无法编写具有“幼稚”低效算法的高效代码。正如我在回答中所说,您需要以不需要读取非连续数组元素的方式组织数组。它可能需要重写大部分代码。当性能是重要因素时,您需要拥有高效的算法
  • @Bananach 你怎么能说算法是垃圾? - 我明白了。你需要重新考虑一下。在您的情况下,数据必须以另一种方式组织。它会使代码更难编写吗? - 是的,但它通常是性能的价格。 但它是你的程序,你可以随心所欲地编写它。但是,如果询问您的意见,您需要准备好听取有关您工作的负面意见。
  • @Bananach:我想知道在构造foo[] 时对ids 进行排序是否会有所帮助。或者以某种方式将其分解并在O(I*J*K) 乘法工作之前或之后应用重新映射,可能是在从临时数组到bar 的路上。尽管K=3 是典型的,但它并不像 matmul 那样糟糕。我必须同意这里的严厉评估:重大重组可能会带来巨大的性能提升,因为该代码固有地具有巨大的瓶颈,可能会降低吞吐量远低于甚至最大化标量 FP 乘法。也许你可以利用我们不知道的ids[]
  • @P__J__ 在您写第二个答案之前,我删除了我的评论。我的观点是,如果我对ids 进行排序,然后进行计算,然后未排序,这只会改变实现,所以如果它加速了我的代码,那将是那种我要求的东西,它不支持 algorithm 不好的观点,而仅支持 implementation 不好的观点。我想我只是误解了“算法”的意思。我这样做是为了进行科学计算,在这种情况下,其背后的算法得到了很好的分析,并且具有完美的缩放性和复杂性等。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-13
  • 2021-11-21
  • 2011-06-26
  • 1970-01-01
  • 1970-01-01
  • 2012-09-02
相关资源
最近更新 更多