这种冗余的load/store优化在C99中是否允许?

12

请考虑以下内容:

extern void bar(int *restrict);

void foo(int *restrict p) {
  int tmp;
  bar(&tmp);
  *p = tmp;
}

C99标准是否允许将foo优化为以下代码?

extern void bar(int *restrict);

void foo(int *restrict p) {
  bar(p);
}

我尝试了gcc、Clang和Intel编译器的-O3模式,但没有生成符合上述优化的代码,这使我怀疑此优化违反了规范。如果不允许使用该优化,请问规范在哪里明确说明了?

注:我的问题受到这个 Stack Overflow 问题的启发。


1
我可以看出,如果它们不是“restrict”指针,这些片段可能彼此不等。但我不知道为什么在“restrict”情况下它们没有被优化。 - Oliver Charlesworth
如果 foo 得到一个指向只写内存(例如内存映射 I/O)的指针会怎样?没有优化是可以的,但有了它 - bar 可能会进行写-读-写操作。 - ugoren
2
@ugoren:对于这样的指针,您绝对应该使用“volatile”限定符。否则,编译器可以在任何情况下进行优化(例如,了解“bar”的主体)。 - jpalecek
2
@jpalecek,foo 只对 *p 进行一次写入,因此编译器无法将其优化掉。当然它不知道 bar 的具体内容,因为它被声明为 extern。所以在这里,bar 可以作为一个适配器,用于在只写指针和 bar 函数之间进行转换,但在这种情况下使用 bar 函数可能是不安全的。 - ugoren
4个回答

18
答案是明确的“不允许”。
考虑如果foo和bar是相互递归的会发生什么。例如,这个实现中的bar:
void bar(int *restrict p)
{
    static int q;
    if (p == &q) {
        printf("pointers match!\n");
    } else if (p == NULL) {
        foo(&q);
    }
}

bar未对p进行解引用,因此关键字restrict不相关。静态变量q显然不能与foo中的自动变量tmp具有相同的地址。因此,foo无法将其参数传回bar,因此给定的优化是不允许的。


1
太好了!一个更简单的“bar”,通过优化可以表现出不同的行为,可能是“void bar(int * restrict p){ if (p == NULL) printf("NULL!\n"); }”。 - Giuseppe Guerrini
1
@GiuseppeGuerrini:但这只有在 foo 的参数为 NULL 时才会产生影响,这会引起未定义的行为。 - Oliver Charlesworth
@Oli Charlesworth 所以在 printf 后面添加一个 "exit(0)"。关键是(正如“ridiculous_fish”已经解释得很清楚),如果函数的实现未知,编译器不能自由地更改函数参数的值。C 参数是“按值传递”的,编译器必须确保函数始终接收到参数值。对于指针,该值是一个地址,但函数对该地址的操作是函数实现的问题。(继续) - Giuseppe Guerrini
@Oli Charleswort(续)一个极端的例子是“void bar(int *restric p){printf(”p =%p \ n“,(void *)p); }”。优化版本的输出将不同。 - Giuseppe Guerrini

1

简要阅读此SO问题此维基百科条目表明,restrict关键字只能在函数参数中起作用。然而,阅读C99标准,特别是第6.7.3.1节,可以清楚地看到restrict适用于使用restrict的整个上下文。因此,通过使用

void foo(int *restrict p);

你正在保证只有通过 p 才能读写内存块的内容。但是,即使有了这个信息,在编译 foo 时,编译器也不知道 bar 将会如何处理它所接收到的信息。例如,请考虑:
void bar (unsigned long long int *p) {
    *p = ((unsigned long long int) p) % 2000;
    }

结果取决于所设置的指针的值,这意味着在编译foo时,无法确定您建议的优化假设是否可以被执行,因为如果执行您建议的优化,则结果将会不同。

restrict 不仅仅适用于同一函数的参数。例如,将指向全局变量的指针传递给 foo,并在 bar 中访问该变量,违反了 restrict 的要求。 - ugoren
另外,你最后的例子有两个问题 - 首先,你不能将 % 应用于指针。其次,使用未经优化的 foo 行为不是明确定义的,因此优化不会破坏任何东西。 - ugoren

1
为了让编译器进行优化,必须确保无论如何实现bar和调用foo的方式如何,定义良好的行为都不会改变。
由于编译器不知道bar的实现和对foo的调用,当它编译foo时,理论上这种情况的存在足以防止优化,即使在现实中没有发生。

以下是这种情况的示例。重要的点是: 1. 参数p指向只写内存(例如映射到内存的I/O)。 2. bar不能与只写指针一起使用(可能会将其写入再读回来,期望相同的值)。 函数foo可与只写指针一起安全使用,因为它只写入p。即使bar不安全,这也是正确的,因为bar永远不会得到p。使用建议的优化,bar会得到p,这可能会引起麻烦。

这是一个包含 bar 和调用 foo 的文件示例。

static int increment;

void bar(int *restrict p) {
    (*p)=0;
    if (increment) (*p)++;
}

void foo(int *restrict p);

int main(int ac, char **av) {
    int *p = get_io_addr();    /* Get a write-only memory mapped I/O address */
    increment = atoi(av[1]);
    foo(p);
    return 0;
}

1
好观点。但请注意,(实际上非常聪明的)编译器可以将“bar”翻译成类似于“*p = increment ? 1: 0;” 的东西。除非指针是“volatile”,但这不是我们的情况 :-( - Giuseppe Guerrini
但编译foo的编译器不能假设编译bar的编译器会进行此优化,对吗? @GiuseppeGuerrini - ugoren
1
@urogen:如果指针不是“volatile”,编译器可以自由地优化其使用,只需确保“p”的最终值的正确性,而不管在“bar”内部实际读取或写入变量的次数。在您展示的情况下,p必须声明为“volatile”,因为对它的优化将无法起作用。特别是,在您的示例中,您有一个只写地址,根本不应该被读取!如果您不通过“volatile”告知编译器此问题,它可以假定“p = 1; x=*p; *p=2”等同于“x=1; *p=2”。 - Giuseppe Guerrini
@GiuseppeGuerrini,哪个p需要声明为volatile?肯定不是在bar中,因为它不能与只写指针一起使用(在我的示例中也没有)。在foo中,我不明白为什么需要volatile - 它只访问p一次。 - ugoren
让我们在聊天中继续这个讨论:http://chat.stackoverflow.com/rooms/31275/discussion-between-giuseppe-guerrini-and-ugoren - Giuseppe Guerrini
显示剩余3条评论

0
这两个代码不等价:在第一个情况下,函数“bar”接收指针(并且可能使用值)“tmp”,它是一个本地(未初始化!)变量。在第二种情况下,“bar”直接在“p”上工作,并且通常会在“*p”中找到不同的值。如果“bar”函数将其参数声明为“仅输出”(例如,在M $ VS中通过OUT宏),则变量的初始值将被(应该)忽略。
(注意:大多数VS版本实际上将OUT宏定义为空。太遗憾了...)

为什么您认为bar在写入其参数之前会从中读取?让我们只看那些没有C99规范涵盖的未定义行为的情况。 - zr.
2
@GiuseppeGuerrini 我认为编译器可以假设如果 bar 使用了它的参数,那么它必须首先对其进行写入。如果 bar 在写入之前进行读取,则行为是未定义的,因此编译器所做的一切都是有效的。如果 bar 先写入,那么就我所看到的来说,两种变体产生相同的可观察行为。 - Daniel Fischer
在我看来,编译器不能做出任何假设。让我们考虑一下“bar”的实现:void bar(int *restrict p) { printf("%d\n", *p); p=1234; }。该函数应该打印p的原始值而不是1234。 - Giuseppe Guerrini
2
如果参数是未初始化的自动变量的地址,则其行为是未定义的。编译器可以假设您的代码不会调用 UB。 - Daniel Fischer
1
实际上,一个好的编译器应该产生一个警告(是的,行为是未定义的)。无论如何,编译器不应该应用任何特定的优化,因为通常它不知道“bar”对tmp地址的操作。 - Giuseppe Guerrini
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接