关于消除C中内联函数指针操作的编译器优化?

Eon*_*nil 5 c compiler-construction optimization pointers

如果此函数Func1内联,

inline int Func1 (int* a)
{
    return *a + 1;
}

int main ()
{
    int v = GetIntFromUserInput(); // Unknown at compile-time.   
    return Func1(&v);
}
Run Code Online (Sandbox Code Playgroud)

我可以期待智能编译器消除指针操作吗?(&a*a)正如我猜的,该函数将转换为这样的东西,

int main ()
{
    int v = GetIntFromUserInput(); // Unknown at compile-time.
    int* a = &v;
    return *a + 1;
}
Run Code Online (Sandbox Code Playgroud)

最后,

int main ()
{
    int v = GetIntFromUserInput(); // Unknown at compile-time.
    return v + 1;
}
Run Code Online (Sandbox Code Playgroud)

指针操作很容易被消除.但我听说指针操作是特殊的,无法优化.

650*_*502 7

是的,正如Wallyk所说,编译器能够在这种情况下删除无用的操作.

但是,您必须记住,当您指定函数签名时,从问题域到C的转换中会丢失某些内容.请考虑以下函数:

void transform(const double *xyz, // Source point
               double *txyz,      // Transformed points
               const double *m,   // 4x3 transformation matrix
               int n)             // Number of points to transform
{
    for (int i=0; i<n; i++)
    {
        txyz[0] = xyz[0]*m[0] + xyz[1]*m[3] + xyz[2]*m[6] + m[9];
        txyz[1] = xyz[0]*m[1] + xyz[1]*m[4] + xyz[2]*m[7] + m[10];
        txyz[2] = xyz[0]*m[2] + xyz[1]*m[5] + xyz[2]*m[8] + m[11];
        txyz += 3; xyz += 3;
    }
}
Run Code Online (Sandbox Code Playgroud)

我认为意图是明确的,但是编译器必须是偏执的,并且认为生成的代码必须完全按照C语义描述,即使在当然不是转换点数组的原始问题的情况下,例如:

  • txyzxyz指向相同的内存地址,或者可能是它们指向内存中的相邻双精度数
  • m指着这个txyz区域

这意味着,对于上述功能C编译器被强制为假设后的各写入txyz任何的xyzm可以改变,因此这些值不能自由顺序加载.因此,结果代码将无法利用并行执行,例如树坐标的计算,即使CPU允许这样做.

这种别名的情况非常普遍,以至于C99引入了一个特定的关键字,以便能够告诉编译器没有任何奇怪的意图.将restict关键字放在声明中txyz并使mat编译器放心使用其他方法无法访问指向的内存,然后允许编译器生成更好的代码.

然而,这种"偏执"的行为仍然是所有操作都必须确保正确性的,例如,如果您编写代码

 char *s = malloc(...);
 char *t = malloc(...);
 ... use s and t ...
Run Code Online (Sandbox Code Playgroud)

编译器无法知道两个内存区域是否会重叠,或者更好地说,没有办法在C语言中定义签名来表示返回值的概念malloc是"非重叠".这意味着偏执的编译器会在后续代码中思考任何对指向的内容的写入s都可能会覆盖指向的数据t(即使你没有超过传递给malloc我的大小;-)).

在您的示例中,即使是偏执的编译器也允许这样做

  1. 除非将它作为参数,否则没有人会知道局部变量的地址
  2. 在读取和添加计算之间不执行未知的外部代码

如果这些点都丢失了,那么编译器必须考虑奇怪的可能性; 例如

int a = malloc(sizeof(int));
*a = 1;
printf("Hello, world.\n");
// Here *a could have been changed
Run Code Online (Sandbox Code Playgroud)

这种疯狂的想法是必要的,因为malloc知道地址a; 因此它可以将此信息传递给printf打印后的字符串,该字符串可以使用该地址来更改位置的内容.这看起来显然是荒谬的,可能是库函数声明可能包含一些特殊的不可移植的技巧,但它一般是正确的必要(想象malloc并且printf是两个用户定义的函数而不是库函数).

所有这些模糊是什么意思?是的,在您的情况下,允许编译器进行优化,但是很容易消除这种可能性; 例如

inline int Func1 (int* a)
{
    printf("pointed value is %i\n", *a);
    return *a + 1;
}

int main ()
{
    int v = GetIntFromUserInput();   // Assume input value is non-determinable.
    printf("Address of v is %p\n", &v);
    return Func1(&v);
}
Run Code Online (Sandbox Code Playgroud)

是一个简单的代码变体,但在这种情况下,编译器不能避免假设第二次printf调用可能已经改变了指向的内存,即使它只传递了指向的值而不是地址(因为第一次调用printf传递了地址和所以编译器必须假设该函数可能已存储该地址以便以后使用它来改变变量).

C和C++中一个非常常见的误解是,使用const指针或(在C++中)引用自由使用关键字将有助于优化器生成更好的代码.这完全是假的:

  1. 在声明const char *s中没有说明尖锐的角色将是恒定的; 它简单地说使用该指针更改指向字符是错误的.换句话说,const在这种情况下,简单地意味着指针是"只读"但并不表示,例如,其他指针可用于改变指向的相同内存s.
  2. 在C(和C++)中,将指针(或引用)中的常量"抛弃"为常量是合法的.因此,偏执的编译器必须假设即使一个函数只是一个const int *函数可以存储该指针,以后可以用它来改变指向的内存.

const带指针(和C++引用)的关键字仅用于帮助程序员避免无意中使用被认为仅用于读取的指针.执行此检查后const,优化程序会忘记此关键字,因为它对语言的语义没有任何影响.

有时您可能会发现另一个对const关键字的愚蠢使用,其参数表明参数的值无法更改; 例如void foo(const int x).这种用法对签名没有真正的哲学意义,只是对被调用函数的实现有点烦恼:参数是值的副本,调用者不应该关心被调用函数是否会改变该副本或不...被调用的函数仍然可以复制参数并更改该副本,因此无论如何都无法获得.

回顾......编译器看到的时候

void foo(const int * const x);
Run Code Online (Sandbox Code Playgroud)

我们仍然必须假设foo可能会存储传递指针的副本,并且可以使用此副本来更改x当您调用任何其他未知函数时立即或稍后指向的内存.

由于语义语义的定义方式,因此需要这种程度的偏执.

理解这个"别名"问题非常重要(可以有不同的方法来改变相同的可写内存区域),特别是在C++中,即使在逻辑上,有一个常见的反模式传递const引用而不是值函数应该接受一个值.如果您还在使用C++,请参阅此答案.

所有这些都是在处理指针或引用时,优化器的自由度远低于本地副本的原因.


wal*_*lyk 5

它可能发生是合理的。例如,gcc -O3这样做:

.globl main
        .type   main, @function
main:
        pushl   %ebp
        movl    %esp, %ebp
        andl    $-16, %esp
        call    GetIntFromUserInput
        movl    %ebp, %esp
        popl    %ebp
        addl    $1, %eax
        ret
Run Code Online (Sandbox Code Playgroud)

请注意,它从函数中获取返回值,加一,然后返回。

有趣的是,它还编译了一个 Func1,可能因为inline它看起来应该具有 的含义static,但是外部函数(如 GetIntFromUserInput)应该能够调用它。如果我添加static(并离开inline),它会删除函数的代码。