簡體   English   中英

嚴格的混疊違規和分析

[英]Strict aliasing violation and analysis

我知道違反嚴格的別名規則可能會導致很多麻煩。 其中之一是某種“寄存器緩存/內存不一致”。 以下代碼是一個示例:

#include <iostream>
#include <vector>
#include <cstring>
#include <numeric>


struct F { float x, y; };


int main()
{
  int input;
  std::cin >> input;
  std::cerr << "\n";
  int size = input;
  
  
  constexpr int cap = 4;
  int64_t z[cap]; 
  size = size > cap ? cap : size;
  std::iota(z, z + size, 0);
  
  
  std::cerr << "Initial z[] = ";
  for(int i = 0; i < size; ++i) std::cerr << z[i] << ", ";
  std::cerr << "\n\n";
  
  
  // Type punning, undefined behavior.
  F *f = (F*)z;
  for(int i = 0; i < size; ++i)
  {
    f[i].x = 3.14 * (i + 1);      
    f[i].y = 3.14 * (size - i);
  }
  
  
  std::cerr << "After writing floats z[] = ";
  for(int i = 0; i < size; ++i) std::cerr << z[i] << ", ";
  
  
  while(true);
  return 0;
}

使用 gcc-8.3 (mingw64) 編譯代碼,

system("C:/rtools40/mingw64/bin/g++.exe -std=gnu++17 -Ofast -o Ofast.exe tmp2.cpp")

在 64 位 Windows 10、Intel i9-9980HK 上,這是 output: 在此處輸入圖像描述

寫入浮點數后, z[2]仍然為 2,這是錯誤的,但也是意料之中的。

標准規定任何char*都可以指向由所有類型的其他指針指向的 memory 塊,因此必須正確確認通過該char*進行的讀取和寫入。 於是我修改了上面的代碼:

#include <iostream>
#include <vector>
#include <cstring>
#include <numeric>


struct F { float x, y; };


int main()
{
  int input;
  std::cin >> input;
  std::cerr << "\n";
  int size = input;
  
  
  constexpr int cap = 4;
  int64_t z[cap]; 
  size = size > cap ? cap : size;
  std::iota(z, z + size, 0);
  
  
  std::cerr << "Initial z[] = ";
  for(int i = 0; i < size; ++i) std::cerr << z[i] << ", ";
  std::cerr << "\n\n";
  
  
  // Undefined behavior.
  F *f = (F*)z;
  for(int i = 0; i < size; ++i)
  {
    f[i].x = 3.14 * (i + 1);      
    f[i].y = 3.14 * (size - i);
  }
  
  
  // The following branch is runtime dependent so cannot be pruned by an
  // aggressive but compliant compiler (ACC).
  // The goal is to make any ACC "fear" that z[]'s contents can be altered
  // via another char*.
  // This complies the strict aliasing rule since a char* can point to
  // any memory block pointed by another pointer of any type, and read / write
  // the memory thereafter.
  if(input > 2000000000) // Little chance to enter.
  {

    char dummy[cap * sizeof(int64_t)];
    int Nbytes = size * sizeof(int64_t);


    char *zchar = (char*)z; // Set a char* pointer to z[]
    std::memcpy(dummy, zchar, Nbytes);


    // Read and write zchar[], do some dummy arithmetics:
    for(int i = 0; i < Nbytes; ++i) zchar[i] &= zchar[Nbytes - 1 - i];
    int S = std::accumulate(zchar, zchar + Nbytes, 0);


    // Print it so this whole dummy thing cannot be pruned by ACC.
    std::cerr << "dummy result = " << S << "\n\n";


    // Recover.
    std::memcpy(zchar, dummy, Nbytes);
  }
  
  
  // Because the same memory block as z[] could be rewritten via a char*, 
  // the ACC has to achieve some sort of register-cache/memory coherence, 
  // thus the right output.
  std::cerr << "After writing floats z[] = ";
  for(int i = 0; i < size; ++i) std::cerr << z[i] << ", ";
  
  
  while(true);
  return 0;
}

結果變得正確: 在此處輸入圖像描述

我什至可以將虛擬分支簡化為

if(input > 2000000000) // Little chance to enter.
{
   int S = std::accumulate((char*)z, (char*)(z + size), int(0));
   std::cerr << "dummy result = " << S << "\n\n";
}

它仍然產生正確的結果。

我關於嚴格別名規則的理由是否正確? 以上是防止類型雙關語可能帶來的“寄存器緩存/內存不連貫”問題的有效方法嗎?

謝謝!

當您說“寄存器緩存/內存一致性”時,我不確定您在想什么。

別名違規的問題很簡單(與所有未定義的行為一樣)優化器可能會選擇依賴它永遠不會發生(因為它是 UB)來推斷程序執行的約束並使用它來生成需要的優化機器代碼只有在這些限制下才能發揮作用。

例如,在您沒有通過char*寫入的情況下,優化器可以例如看到通過F*的寫入不可能修改z ,因為這將是一個別名違規。 因此,它可以例如在 output 之后通過F*重新排序寫入。

或者,編譯器可能會看到z首先寫入了可以在編譯時計算的值,它可能會記住這些值以進行優化。 然后它可以忽略通過F*進行的寫入,由於 UB,這是不可能的,最后看到 output 它可以簡單地選擇 output 常量對應於它“知道”在z中的值。

您的保護方法可能會阻礙其中一些優化,但編譯器可能會選擇采用其他優化方法。

例如,編譯器可能會識別出通過F*進行的寫入是 UB 並由此得出結論,唯一可能的值size可能是0 ,因為循環體可能永遠不會在非 UB 程序中執行。

然后它可以使用這些知識來優化整個程序:

int main()
{
  int input;
  std::cin >> input;
  std::cerr << "\n";
  std::cerr << "Initial z[] = ";
  std::cerr << "\n\n";
  std::cerr << "After writing floats z[] = ";
  
  return 0;
}

使用您的方法,您只能希望編譯器在某些時候不會獲得足夠復雜的優化能力來做出此決定(而且我不確定目前沒有編譯器能夠並且願意這樣做。)

如果您想使用違反別名規則的構造,那么您需要確保您的編譯器根本不依賴它進行優化。 編譯器通常有一個標志告訴他們不要這樣做,例如-fno-strict-aliasing用於 GCC,盡管在您的程序中,不僅是別名沖突是一個問題,而且它也是一個問題,沒有實際上任何F object 或F對象數組,您將被允許在其上進行指針運算或訪問成員。 我不確定 GCC 的-fno-strict-aliasing標志通常是否足以保證它不會依賴這兩種 UB 進行優化。


附帶說明: while(true); 在 C++(但不是 C)中也是未定義的行為。 如果沒有 IO、原子或易失性操作,就不可能有無限循環。

例如,只要循環存在 Clang 13 和-O3編譯器資源管理器輸出:

Initial z[] = 0, 1, 2, 

After writing floats z[] = 4690138725358302659, 4668251232723858883, 4632222435709990994, 4210784, 1, 12884901889, 2, 4200237, 140113662963656, 4200160, 0, 4200160, 0, 0, 0, 140113661087923, 140113662942080, 140736215914984, 4295040000, 4198848, 4200160, -8494071215197471174, 4198608, 140736215914976, 0, 0, 8494236860351561274, 8413707913860136506, 0, 0, 0, 1, 140736215914984, 140736215915000, 140113666830736, 0, 0, 4198608, 140736215914976, 0, 0, 4198654, 140736215914968, 28, 1, 140736215916259, 0, 140736215916270, 140736215916410, 140736215916436, 140736215916463, 140736215916489, 140736215916515, 0, 33, 140736217014272, 16, 529267711, 6, 4096, 17, 100, 3, 4194368, 4, 56, 5, 11, 7, 140113666637824, 8, 0, 9, 4198608, 11, 0, 12, 0, 13, 0, 14, 0, 23, 0, 25, 140736215915385, 26, 2, 31, 140736215916525, 15, 140736215915401, 0, 0, 0, -5663448171308038656, 2109109041276691562, 14696481348417631, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8391735729100685312, 4921308987509732720, 6436278639021083743, 8011689642975907935, 7597692896546878576, 7813877729437312364, 7020094974597624431, 3328210922030065518, 8011686456465305392, 7597692896546878576, 7813877729437312364, 7020094974597624431, 3328210922030065518, 4193470700803862320, 7885630528017166127, 8675390226550253936, 7147056913697434736, 3329058620635505004, 3918810539134823984, 5719376094260428852, 7150963379136975952, 8605359904538979439, 4707178968379521377, 5642809484591980366, 4427723895174544723, 5548561706083904609, 5283936564644036947, 8028914707716066895, 8320788952091016562, 5786948835902442496, 8026326388909754708, 7023201308806115180, 4415020012612383609, 8011672841536692527, 32420700043113589, 0, 

而且我不知道編譯器資源管理器是否截斷 output: https://godbolt.org/z/bcq5vKonG

暫無
暫無

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

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