为什么 C 编译器不能优化更改 const 指针的值,假设指向同一变量的两个指针是非法的/UB?

问题描述

最近我偶然发现了 Rust 和 C 之间的比较,它们使用以下代码

bool f(int* a,const int* b) {
  *a = 2;
  int ret = *b;
  *a = 3;
  return ret != 0;
}

在 Rust(相同的代码,但使用 Rust 语法)中,它生成以下汇编代码

    cmp      dword ptr [rsi],0 
    mov      dword ptr [rdi],3 
    setne al                    
    ret

在使用 gcc 时,它会产生以下结果:

   mov      DWORD PTR [rdi],2   
   mov      eax,DWORD PTR [rsi]
   mov      DWORD PTR [rdi],3        
   test     eax,eax                  
   setne al                           
   ret

文本声称 C 函数无法优化第一行,因为 ab 可能指向相同的数字。在 Rust 中这是不允许的,因此编译器可以将其优化掉。

现在我的问题:

函数采用 const int*,即 a pointer to a const int。我读过 this question 并指出用指针修改 const int 会导致编译器警告和 UB 中最糟糕的转换。

如果我用两个指向同一个整数的指针调用这个函数,它会产生一个 UB 吗?

为什么 C 编译器不能优化第一行,假设两个指向同一个变量的指针是非法的/UB?

Link to godbolt

解决方法

为什么C编译器不能优化第一行,假设两个指向同一个变量的指针是非法的/UB?

因为您没有指示 C 编译器这样做 - 允许做出这种假设。

C 有一个类型限定符,它叫做 restrict,大致意思是:这个指针不与其他指针重叠(不是完全,而是一起玩)。

汇编输出

bool f(int* restrict a,const int* b) {
  *a = 2;
  int ret = *b;
  *a = 3;
  return ret != 0;
}

        mov     eax,DWORD PTR [rsi]
        mov     DWORD PTR [rdi],3
        test    eax,eax
        setne   al
        ret

... 删除分配 *a = 2

来自https://en.wikipedia.org/wiki/Restrict

在 C 编程语言中,restrict 是一个可以在指针声明中使用的关键字。通过添加这个类型限定符,程序员向编译器提示,在指针的生命周期内,只有指针本身或直接派生自它的值(例如指针 + 1)将用于访问它指向的对象。

,

函数 int f(int *a,const int *b); 承诺不会通过该指针改变 b 的内容...它没有承诺通过 a 访问变量指针。

如果 ab 指向同一个对象,通过 a 更改它是合法的(当然,前提是基础对象是可修改的)。

示例:

int val = 0;
f(&val,&val);
,

虽然其他答案提到了 C 方面,但仍然值得一看 Rust 方面。使用 Rust,您的代码可能是这样的:

fn f(a:&mut i32,b:&i32)->bool{
    *a = 2;
    let ret = *b;
    *a = 3;
    return ret != 0;
}

该函数接受两个引用,一个可变的,一个不可变的。引用是保证对读取有效的指针,可变引用也保证是唯一的,因此它被优化为

        cmp     dword ptr [rsi],0
        mov     dword ptr [rdi],3
        setne   al
        ret

然而,Rust 也有与 C 的指针等价的原始指针,并且不做这样的保证。以下函数接受原始指针:

unsafe fn g(a:*mut i32,b:*const i32)->bool{
    *a = 2;
    let ret = *b;
    *a = 3;
    return ret != 0;
}

错过了优化并编译为:

        mov     dword ptr [rdi],2
        cmp     dword ptr [rsi],3
        setne   al
        ret

Godbolt Link

,

该函数接受一个 const int*,它是一个指向 const int 的指针。

不,const int* 不是指向 const int 的指针。任何说这话的人都被迷惑了。

  • int* 是一个指向绝对不是 const 的 int 的指针。

  • const int* 是一个指向未知常量的 int 的指针。

  • 没有办法表达指向绝对是 const 的 int 的指针的概念。

如果 C 是一种设计更好的语言,那么 const int * 将是一个指向 const int 的指针,mutable int *(从 C++ 借用一个关键字)将是一个指向非常量 int 的指针,以及int * 将是一个指向未知常量的 int 的指针。删除限定符(即忘记指向类型的某些内容)将是安全的——与真正的 C 语言相反,其中添加 const 限定符是安全的。我没有使用过 Rust,但从另一个答案中的示例中可以看出它使用了类似的语法。

介绍 const 的 Bjarne Stroustrup 最初将其命名为 readonly,更接近其实际含义。 int readonly* 会更清楚地表明它是只读的指针,而不是指向的对象。重命名为 const 让几代程序员感到困惑。

当我有选择时,我总是写foo const*,而不是const foo*,作为readonly*的次佳选择。

,

应该注意的是,这个问题是在讨论 -Ofast 上的优化,以及它是如何做到的。

本质上,函数的 C 编译器不知道可能传递给它的完整离散地址集,因为直到链接时/运行时才知道该函数可以从多个翻译单元调用,因此它会考虑处理 ab 可能指向的任何合法地址,当然也包括它们重叠的情况。

因此,您需要使用 restrict 来告诉它更新 a (该函数允许这样做,因为它不是指向常量的指针,但即使如此,该函数也可以抛弃 const)不更新 b 指向的值,该值需要包含在与 0 的比较中,因此在比较需要继续之前发生的存储到 a,而在 rust 中是默认值假设是限制。然而,该函数的编译器确实知道 *a*(a+1-1) 相同,因此不会产生 2 个单独的存储,但它不知道 ab 是否重叠.