簡體   English   中英

C 按位與 -O0 和 -O2 給出不同的結果

[英]C bitwise AND gives different result with -O0 and -O2

我正在使用 Bochs 和 DOSBox 作為參考來開發 PC 模擬器。

在模擬“NEG Ed”指令(雙字的二進制補碼否定)時,如果我使用-O0而不是-O2編譯,我會得到不同的結果。

這是一個只有相關位的測試程序:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>

int main(int argc, const char **argv)
{
    uint32_t value = strtol(argv[1], NULL, 16);
    uint32_t negation = -(int32_t)(value);
    bool sign = negation & 0x80000000;

    printf("value=%X, negation=%X, sign=%X\n", value, negation, sign);
    
    return 0;
}

-(int32_t)(value); 部分取自 Bochs 的NEG_EdM() function; 對於等效操作,DOSBox 不會強制轉換為帶符號的 int。

如果您使用-O2選項使用 GCC 10 編譯此程序並使用十六進制值0x80000000作為輸入,您將得到錯誤的sign結果:

value=80000000, negation=80000000, sign=0

使用-O0編譯時,結果是正確的:

value=80000000, negation=80000000, sign=1

這是未定義的行為嗎?

據我所知,有符號和無符號整數的轉換是明確定義的,無符號值的按位 & 也是如此。

未定義行為的來源

問題的關鍵部分在於否定-(int32_t)value 1

此時, value 80000000 16 (2 31 )。 由於這在int32_t中無法表示,因此轉換由 C 2018 6.3.1.3 3 管理,這表示行為是實現定義的。 GCC 10.2 將其定義為模 2 N包裝,其中目標寬度為N位。 將 80000000 16包裝到int32_t模 2 32產生 −80000000 16

然后應用否定運算符- -80000000 16的數學否定當然是 80000000 16 ,但這在int32_t中無法表示。 2行為受 C 2018 6.5 5 約束:

如果在計算表達式期間出現異常情況(即,如果結果未在數學上定義或不在其類型的可表示值范圍內),則行為未定義。

因此,否定具有未定義的行為。 當使用-O0時,編譯器生成簡單的直接代碼。 Godbolt 顯示它會生成一個否定指令,該指令會生成 output 80000000 16用於輸入位 80000000 16 (將 -80000000 16表示為帶符號的 32 位整數)。 當使用-O2時,編譯器會對程序進行復雜的分析和轉換,缺乏定義的行為使編譯器可以自由地產生任何結果。 事實上, Godbolt 表明否定指令不存在 實際上,編譯器“知道”取反int32_t值永遠不會產生在具有定義行為的程序中設置 2 31位的結果。

優化討論

考慮int32_t中可表示的值的范圍是 -2 31到 2 31 -1。 這些的數學否定是 -(2 31 -1) 到 2 31 但是,2 31溢出,導致異常情況。 不溢出的結果范圍是 -(2 31 -1) 到 2 31 -1。 因此,在具有已定義行為的程序中,只會出現這些結果,因此編譯器可能會像只出現這些結果一樣進行優化。 在這些結果中沒有一個是 2 31位集。 因此,在具有定義行為的程序中, negation & 0x80000000始終為零,編譯器可能會基於此生成代碼。

使固定

看來您想要測試符號位是否會設置在使用二進制補碼取反的int32_t中,即包裝結果模 2 32 為此,可以使用無符號算術。 如果x是一個int32_t值或一個uint32_t ,其中包含表示此類值的位,則可以通過以下任一方式獲得取反值的符號位:

bool sign = - (uint32_t) x & 0x80000000u;
bool sign = - (uint32_t) x >> 31;

腳注

1我們推斷long比 32 位更寬。 Were it not, strtol("0x80000000", NULL, 16) would return LONG_MAX , per C 2018 7.22.1.4 8. That would be representable in uint32_t and int32_t , so value would be initialized to LONG_MAX , converting to int32_t would keep that value , negation將是 - LONG_MAX ,並且在程序的優化和未優化版本中, sign都將為零。

2如果int32_tint窄,則操作數將在取反之前提升為int ,並且數學結果將是可表示的。 您使用的 GCC 版本和選項並非如此,我們可以從觀察結果中推斷出。

您的代碼中存在一些問題:

  • strtol("0x80000000", NULL, 16)返回的值取決於long類型的范圍:如果long類型為 32 位,則返回值應為LONG_MAX ,即2147483647 ,而如果long更大,則返回2147483648 . 將這些值轉換為uint32_t不會在uint32_t的范圍內更改值。 您的系統上的long類型似乎有 64 位。 您可以使用strtoul()而不是strtol()來避免這種實現定義的行為。

  • 不需要中間轉換為(int32_t) :否定無符號值是明確定義的,並且-0x80000000對於uint32_t類型的值為0x80000000

  • 此外,這種轉換會適得其反,並且觀察到的行為的可能原因是否定值INT32_MIN由於有符號算術溢出而具有未定義的行為。 啟用優化后,編譯器確定您正在提取符號,就好像通過bool sign = -(int32_t)value < 0並將此表達式簡化為bool sign = (int32_t)value > 0 ,這對於除INT32_MIN之外的所有值都是正確的編譯器認為任何行為都可以,因為無論如何該行為都是未定義的。 您可以在Godbolt 的 Compiler Explorer上查看代碼。

  • 你使用bool類型而不包括<stdbool.h> :程序不應該編譯。 這是復制/粘貼錯誤還是您編譯為 c++? C99 _Bool語義在初始化語句中添加了一個隱式測試,但最好讓它顯式並編寫:

     bool sign = (negation & 0x80000000);= 0;
  • 最后,將uint32_t值傳遞給printf以獲取%X轉換說明符。 如果平台上的int類型少於 32 位,則這是不正確的。 使用<inttypes.h>中的宏。

試試這個修改后的版本:

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <inttypes.h>

int main(int argc, const char **argv)
{
    uint32_t value = strtoul(argv[1], NULL, 16);
    uint32_t negation = -value;
    bool sign = (negation & 0x80000000) != 0;

    printf("value=%"PRIX32", negation=%"PRIX32", sign=%d\n", value, negation, sign);
    
    return 0;
}

您不幸的經歷源於有符號算術溢出的未定義行為。 編譯器可以利用未定義的行為來實現高級優化,例如刪除for (int i = 0; i > 0; i++)中的最終測試以及更明顯但非平凡的優化,例如轉換void f(int i) { int j = i * 2 / 2; ... void f(int i) { int j = i * 2 / 2; ...int j = i; 對於超過0x3fffffff的值,這可能會表現出不同的行為。

其他語言(即:java)嘗試刪除未定義的行為並完全指定二進制補碼實現和行為,因此不會執行這些優化。

標准 C 語言委員會似乎支持更多的優化,但代價是邊境案件中的一些意外情況,這可能很難發現和解決。 你的例子是這個問題的完美例證。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM