【问题标题】:using restrict qualifier with C99 variable length arrays (VLAs)将限制限定符与 C99 可变长度数组 (VLA) 一起使用
【发布时间】:2015-05-19 07:04:33
【问题描述】:

我正在探索 C99 中简单循环的不同实现如何根据函数签名自动矢量化。

这是我的代码:

/* #define PRAGMA_SIMD _Pragma("simd") */
#define PRAGMA_SIMD

#ifdef __INTEL_COMPILER
#define ASSUME_ALIGNED(a) __assume_aligned(a,64)
#else
#define ASSUME_ALIGNED(a)
#endif

#ifndef ARRAY_RESTRICT
#define ARRAY_RESTRICT
#endif

void foo1(double * restrict a, const double * restrict b, const double * restrict c) 
{ 
    ASSUME_ALIGNED(a);
    ASSUME_ALIGNED(b);
    ASSUME_ALIGNED(c);
    PRAGMA_SIMD
    for (int i = 0; i < 2048; ++i) {
        if (c[i] > 0) {
            a[i] = b[i];
        } else {
            a[i] = 0.0;
        } 
    }
}

void foo2(double * restrict a, const double * restrict b, const double * restrict c) 
{ 
    ASSUME_ALIGNED(a);
    ASSUME_ALIGNED(b);
    ASSUME_ALIGNED(c);
    PRAGMA_SIMD
    for (int i = 0; i < 2048; ++i) {
        a[i] = ((c[i] > 0) ? b[i] : 0.0);
    }
}

/* Undetermined size version */

void foo3(int n, double * restrict a, const double * restrict b, const double * restrict c) 
{ 
    ASSUME_ALIGNED(a);
    ASSUME_ALIGNED(b);
    ASSUME_ALIGNED(c);
    PRAGMA_SIMD
    for (int i = 0; i < n; ++i) {
        if (c[i] > 0) {
            a[i] = b[i];
        } else {
            a[i] = 0.0;
        } 
    }
}

void foo4(int n, double * restrict a, const double * restrict b, const double * restrict c) 
{ 
    ASSUME_ALIGNED(a);
    ASSUME_ALIGNED(b);
    ASSUME_ALIGNED(c);
    PRAGMA_SIMD
    for (int i = 0; i < n; ++i) {
        a[i] = ((c[i] > 0) ? b[i] : 0.0);
    }
}

/* Static array versions */

void foo5(double ARRAY_RESTRICT a[2048], const double ARRAY_RESTRICT b[2048], const double ARRAY_RESTRICT c[2048]) 
{ 
    ASSUME_ALIGNED(a);
    ASSUME_ALIGNED(b);
    ASSUME_ALIGNED(c);
    PRAGMA_SIMD
    for (int i = 0; i < 2048; ++i) {
        if (c[i] > 0) {
            a[i] = b[i];
        } else {
            a[i] = 0.0;
        } 
    }
}

void foo6(double ARRAY_RESTRICT a[2048], const double ARRAY_RESTRICT b[2048], const double ARRAY_RESTRICT c[2048]) 
{ 
    ASSUME_ALIGNED(a);
    ASSUME_ALIGNED(b);
    ASSUME_ALIGNED(c);
    PRAGMA_SIMD
    for (int i = 0; i < 2048; ++i) {
        a[i] = ((c[i] > 0) ? b[i] : 0.0);
    }
}

/* VLA versions */

void foo7(int n, double ARRAY_RESTRICT a[n], const double ARRAY_RESTRICT b[n], const double ARRAY_RESTRICT c[n]) 
{ 
    ASSUME_ALIGNED(a);
    ASSUME_ALIGNED(b);
    ASSUME_ALIGNED(c);
    PRAGMA_SIMD
    for (int i = 0; i < n; ++i) {
        if (c[i] > 0) {
            a[i] = b[i];
        } else {
            a[i] = 0.0;
        } 
    }
}

void foo8(int n, double ARRAY_RESTRICT a[n], const double ARRAY_RESTRICT b[n], const double ARRAY_RESTRICT c[n]) 
{ 
    ASSUME_ALIGNED(a);
    ASSUME_ALIGNED(b);
    ASSUME_ALIGNED(c);
    PRAGMA_SIMD
    for (int i = 0; i < n; ++i) {
        a[i] = ((c[i] > 0) ? b[i] : 0.0);
    }
}

当我编译时

$ icc -O3 -std=c99 -opt-report5 -mavx -S foo.c 
icc: remark #10397: optimization reports are generated in *.optrpt files in the output location

我看到 VLA 案例不是自动矢量化的,但是当我添加标志以断言没有别名 -fno-alias 时,它们是。因此,我得出结论,我应该在源代码中规定这个,所以我尝试通过编译来做到这一点

$ icc -O3 -std=c99 -opt-report5 -mavx -DARRAY_RESTRICT=restrict -S foo.c 
icc: remark #10397: optimization reports are generated in *.optrpt files in the output location

编译器错误输出包括

foo.c(98): error: "restrict" is not allowed
void foo7(int n, double ARRAY_RESTRICT a[n], const double ARRAY_RESTRICT b[n], 
const double ARRAY_RESTRICT c[n]) 

             ^

但如您所见,我的 VLA 参数不允许使用限制。

所以我的问题是:有没有办法在 ISO C 中断言 VLA 没有别名?

请注意,我可以使用编译指示在源代码中断言没有别名 - 例如simdomp simdivdep 等 - 并获得我想要的自动矢量化,但这些不是 ISO C。

在这种情况下,ISO C 是指 C 的最新版本,在撰写本文时当然是 C11。

【问题讨论】:

  • foo5() 中的数组并不是真正的 VLA。
  • foo[5678] 和 foo[78] 出现语法错误有 VLA 参数。虽然 foo[56] 不使用 VLA,但同样的问题也适用于限制限定符的使用。
  • ISO/IEC 9899:2011 第 6.7.3.1 节 restrict 的正式定义 是否有帮助:D 成为一个普通标识符的声明,它提供了一种方法将对象P 指定为指向T 类型的限制限定指针。 数组abc 是否满足该定义。我承认我不确定,但我认为不是。
  • 另外,第 6.7.6.3 节有示例 5,它表示以下函数原型声明符是等效的:void f(double (* restrict a)[5]);void f(double a[restrict][5]);void f(double a[restrict 3][5]);void f(double a[restrict static 3][5]);。您可能需要从那里进行一些追逐......
  • 这是标准中唯一出现restrict 与数组类型直接关联的地方。 §6.7.6 一般用于声明符,§6.7.6.2 用于数组声明符,在我看来,restrict 必须出现在数组维度的第一个组件内。在您的上下文中,它应该是:void foo7(int n, double a[ARRAY_RESTRICT n], const double b[ARRAY_RESTRICT n], const double c[ARRAY_RESTRICT n]),我相信(但如果没有看到标准中的示例并且您提出问题,我不会相信!)。另请注意,这适用于数组以及 VLA。

标签: c99 simd variable-length-array restrict-qualifier auto-vectorization


【解决方案1】:

您的原始代码对我来说很失败,并显示如下消息:

 void foo7(int n, double ARRAY_RESTRICT a[n], const double ARRAY_RESTRICT b[n], const double ARRAY_RESTRICT c[n])
 ^
restrict.c:126:1: error: invalid use of ‘restrict’
restrict.c:126:1: error: invalid use of ‘restrict’
restrict.c:145:1: error: invalid use of ‘restrict’

传输 cmets 的选定部分

§6.7.6.3 函数声明符(包括原型)有示例 5,它说明以下函数原型声明符是等价的:

void f(double (* restrict a)[5]);
void f(double a[restrict][5]);
void f(double a[restrict 3][5]);
void f(double a[restrict static 3][5]);

这是标准中唯一与数组类型直接关联的地方。 §6.7.6 一般用于声明符,§6.7.6.2 用于数组声明符,在我看来,限制必须出现在数组维度的第一个组件内。在您的上下文中,它应该是:

void foo7(int n, double a[ARRAY_RESTRICT n],
           const double b[ARRAY_RESTRICT n],
           const double c[ARRAY_RESTRICT n])

如果没有看到标准中的示例并且您提出问题,我不会相信这种表示法!请注意,这适用于数组以及 VLA。

此修改后的代码,基于注释,在相同的编译选项下编译干净:

gcc -g -O3 -std=c11 -Wall -Wextra -Wmissing-prototypes -Wstrict-prototypes \
    -Wold-style-definition -Wold-style-declaration -Werror -c new.restrict.c

编译选项要求事先声明非静态函数,因此需要在文件顶部声明。我还在源代码中强制使用#define ARRAY_RESTRICT restrict,而不是将其保留为编译选项。

编译器是在 Ubuntu 14.04 衍生版本上运行的 GCC 4.9.2。

文件new.restrict.c

/* #define PRAGMA_SIMD _Pragma("simd") */
#define PRAGMA_SIMD

#ifdef __INTEL_COMPILER
#define ASSUME_ALIGNED(a) __assume_aligned(a, 64)
#else
#define ASSUME_ALIGNED(a)
#endif

#define ARRAY_RESTRICT restrict

#ifndef ARRAY_RESTRICT
#define ARRAY_RESTRICT
#endif

void foo1(double *restrict a, const double *restrict b, const double *restrict c);
void foo2(double *restrict a, const double *restrict b, const double *restrict c);
void foo3(int n, double *restrict a, const double *restrict b, const double *restrict c);
void foo4(int n, double *restrict a, const double *restrict b, const double *restrict c);
void foo5(double a[ARRAY_RESTRICT 2048], const double b[ARRAY_RESTRICT 2048], const double c[ARRAY_RESTRICT 2048]);
void foo6(double a[ARRAY_RESTRICT 2048], const double b[ARRAY_RESTRICT 2048], const double c[ARRAY_RESTRICT 2048]);
void foo7(int n, double a[ARRAY_RESTRICT n], const double b[ARRAY_RESTRICT n], const double c[ARRAY_RESTRICT n]);
void foo8(int n, double a[ARRAY_RESTRICT n], const double b[ARRAY_RESTRICT n], const double c[ARRAY_RESTRICT n]);

void foo1(double *restrict a, const double *restrict b, const double *restrict c)
{
    ASSUME_ALIGNED(a);
    ASSUME_ALIGNED(b);
    ASSUME_ALIGNED(c);
    PRAGMA_SIMD
    for (int i = 0; i < 2048; ++i)
    {
        if (c[i] > 0)
        {
            a[i] = b[i];
        }
        else
        {
            a[i] = 0.0;
        }
    }
}

void foo2(double *restrict a, const double *restrict b, const double *restrict c)
{
    ASSUME_ALIGNED(a);
    ASSUME_ALIGNED(b);
    ASSUME_ALIGNED(c);
    PRAGMA_SIMD
    for (int i = 0; i < 2048; ++i)
    {
        a[i] = ((c[i] > 0) ? b[i] : 0.0);
    }
}

/* Undetermined size version */

void foo3(int n, double *restrict a, const double *restrict b, const double *restrict c)
{
    ASSUME_ALIGNED(a);
    ASSUME_ALIGNED(b);
    ASSUME_ALIGNED(c);
    PRAGMA_SIMD
    for (int i = 0; i < n; ++i)
    {
        if (c[i] > 0)
        {
            a[i] = b[i];
        }
        else
        {
            a[i] = 0.0;
        }
    }
}

void foo4(int n, double *restrict a, const double *restrict b, const double *restrict c)
{
    ASSUME_ALIGNED(a);
    ASSUME_ALIGNED(b);
    ASSUME_ALIGNED(c);
    PRAGMA_SIMD
    for (int i = 0; i < n; ++i)
    {
        a[i] = ((c[i] > 0) ? b[i] : 0.0);
    }
}

/* Static array versions */

void foo5(double a[ARRAY_RESTRICT 2048], const double b[ARRAY_RESTRICT 2048], const double c[ARRAY_RESTRICT 2048])
{
    ASSUME_ALIGNED(a);
    ASSUME_ALIGNED(b);
    ASSUME_ALIGNED(c);
    PRAGMA_SIMD
    for (int i = 0; i < 2048; ++i)
    {
        if (c[i] > 0)
        {
            a[i] = b[i];
        }
        else
        {
            a[i] = 0.0;
        }
    }
}

void foo6(double a[ARRAY_RESTRICT 2048], const double b[ARRAY_RESTRICT 2048], const double c[ARRAY_RESTRICT 2048])
{
    ASSUME_ALIGNED(a);
    ASSUME_ALIGNED(b);
    ASSUME_ALIGNED(c);
    PRAGMA_SIMD
    for (int i = 0; i < 2048; ++i)
    {
        a[i] = ((c[i] > 0) ? b[i] : 0.0);
    }
}

/* VLA versions */

void foo7(int n, double a[ARRAY_RESTRICT n], const double b[ARRAY_RESTRICT n], const double c[ARRAY_RESTRICT n])
{
    ASSUME_ALIGNED(a);
    ASSUME_ALIGNED(b);
    ASSUME_ALIGNED(c);
    PRAGMA_SIMD
    for (int i = 0; i < n; ++i)
    {
        if (c[i] > 0)
        {
            a[i] = b[i];
        }
        else
        {
            a[i] = 0.0;
        }
    }
}

void foo8(int n, double a[ARRAY_RESTRICT n], const double b[ARRAY_RESTRICT n], const double c[ARRAY_RESTRICT n])
{
    ASSUME_ALIGNED(a);
    ASSUME_ALIGNED(b);
    ASSUME_ALIGNED(c);
    PRAGMA_SIMD
    for (int i = 0; i < n; ++i)
    {
        a[i] = ((c[i] > 0) ? b[i] : 0.0);
    }
}

【讨论】:

  • 我没有查看基本原理或委员会会议记录,以了解为什么符号需要保持原样。
  • 您还有其他建议吗? double QUALIFIER a[n] 意味着 QUALIFIER 符合 double,而不是 a。与此代码中的 const 示例一样。
  • @MattMcNabb:我不确定你在寻找什么。您是否建议这个建议的解决方案没有“限制”数组以使它们不在重叠内存中?或者您正在寻找其他类型限定符的信息? direct declarator 的定义(在 §6.7.6 中)包括诸如 direct-declarator [ type-qualifier-list opt assignment-expression opt ]direct-declarator [ static type-qualifier-list opt assignment-expression ]direct-declarator [ type-qualifier-list static assignment-expression ]我> ...
  • 我指的是您的评论“基本原理......找出为什么符号 需要 保持原样”以及您关于该符号令人难以置信的其他 cmets,等等。这意味着您对符号感到不满意,所以我想问您是否有其他的符号。
  • @MattMcNabb:哦,我明白了。先验地,我希望 restrict 出现在方括号之外,就像 OP 所做的那样。我不完全清楚为什么它不能在那里处理,但标准的库部分中所有其他出现的restrict 都采用 type * restrict 的形式 - 或者可能是 type ** restrict — 这样关键字就在* 指示指针之后,并且在函数的数组参数声明中没有指示指针的*,所以可能没有一个明智的选择。
【解决方案2】:

此代码中的所有参数都没有可变修改类型。 foo6foo7 是完全相同的函数签名,但 int n 除外。见Why do C and C++ compilers allow array lengths in function signatures when they're never enforced?

这些都是一样的:

void foo8(int n, T *a);
void foo8(int n, T a[16]);
void foo8(int n, T a[n]);

void foo8(int n, T a[]); 的版本几乎相同,但它有一个极端情况,如果T 是不完整的类型,则不允许这样做。

foo8 可以使用 VLA 或非 VLA 调用。

虽然数组声明器具有可变修改类型,但array-of-Tpointer-to-T的调整是在参数类型被计算之前进行的.所以T a[n] 被调整为T *a,它没有被可变修改;但是void foo9(int n, T a[][n]); 确实会为a 生成可变修改类型T (*)[n]


restrict 与数组声明符组合的最简单方法是实际使用指针形式,这里:

void foo8(int n, T *restrict a ) {

尝试void foo8(int n, T restrict a[]); 无效,因为它等同于void foo8(int n, T restrict *a);restrict 是一个限定符,它在语法上与const 等其他限定符的行为相同。

正如 Jonathan Leffler 所说,还有一种替代语法:

void foo8(int n, T a[restrict]) {   // n is optional , as before

在这种情况下,允许以两种不同的方式指定同一事物似乎是多余的,但是 there also exists static 只能与数组声明符(而不是指针声明符)一起使用。如果您想使用staticrestrict 的这种形式,那么别无选择,只能在方括号内添加restrict

void foo8(int n, T a[restrict static n]) { 

需要明确的是,最后一种情况仍然不是可变修改类型; static 是一个承诺,a它是一个指针,指向至少有 n 元素的数组的第一个元素。

另外,当函数被调用时,static 不需要在编译时检查(当然如果编译器确实强制执行会很好)。

最后一点:restrict 在原型 probably has no effect 中,它只在函数定义中有意义。

【讨论】:

  • 关于原型中的restrict 是否有任何影响的有趣外部参考。如果编译器不知道必须强制执行该约束,则编译器不可能强制执行该约束,因此原型中 restrict 的存在允许编译器推断出 foo7(3, &amp;a[0], &amp;a[1], &amp;a[2])(使用问题中的函数)是违反restrict 标准。任何编译器是否真的这样做是一个单独的讨论,但如果原型中省略了restrict,则此类错误只能在定义函数的TU中报告,然后只能在定义之后的调用中报告。
  • @JonathanLeffler 是的,我的另一个问题试图按照这些思路进行讨论。你的结论是有道理的。
  • 您是说编译器不允许使用“a[16]”中包含的“a[]”中不包含的信息吗?自动矢量化?我的目标是尽可能多地告诉编译器,例如,当我使用“a[n]”与“*a”时,我相信我会说更多。
  • @Jeff 这就是a[static 16] 的用途。如果没有static,该函数无法对a 指向的内容做出任何假设(当然,除非编译器正在执行整个程序优化等等)。将大小为2 的数组传递给带有参数a[16] 等的函数是合法的。尽管我不完全了解您想到的优化是什么,除了非空值之外,这些信息会有所不同位。
  • @Jeff 当然编译器可以根据你的代码来决定。 (例如,如果您的代码从 0 循环到 16,则声明的维度无关紧要,编译器可以假定数组有 16 个元素)。以防万一,a[static n] 存在,它表示a 指向至少具有n 元素的数组的第一个元素。除了假设 a 不为空之外,我不知道有任何编译器实际上基于此进行优化。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-12-14
  • 2012-01-29
  • 2011-07-11
  • 2017-04-24
  • 1970-01-01
  • 2017-04-30
  • 2011-12-13
相关资源
最近更新 更多