Quy tắc răng cưa nghiêm ngặt là gì?


804

Khi hỏi về hành vi không xác định phổ biến trong C , đôi khi người ta đề cập đến quy tắc răng cưa nghiêm ngặt.
Bọn họ đang nói gì thế?


12
@Ben Voigt: Các quy tắc răng cưa khác nhau đối với c ++ và c. Tại sao câu hỏi này được gắn thẻ cc++faq.
MikeMB

6
@MikeMB: Nếu bạn kiểm tra lịch sử, bạn sẽ thấy rằng tôi giữ các thẻ theo cách ban đầu, mặc dù một số chuyên gia khác cố gắng thay đổi câu hỏi từ dưới câu trả lời hiện có. Bên cạnh đó, sự phụ thuộc ngôn ngữ và sự phụ thuộc phiên bản là một phần rất quan trọng trong câu trả lời cho "Quy tắc răng cưa nghiêm ngặt là gì?" và biết được sự khác biệt rất quan trọng đối với các nhóm di chuyển mã giữa C và C ++ hoặc viết macro để sử dụng cho cả hai.
Ben Voigt

6
@Ben Voigt: Thật ra - theo như tôi có thể nói - hầu hết các câu trả lời chỉ liên quan đến c và không phải c ++, từ ngữ của câu hỏi chỉ ra sự tập trung vào các quy tắc C (hoặc OP chỉ không biết, rằng có một sự khác biệt ). Đối với hầu hết các quy tắc và Ý tưởng chung là tất nhiên giống nhau, nhưng đặc biệt, khi các công đoàn quan tâm đến câu trả lời không áp dụng cho c ++. Tôi hơi lo lắng, rằng một số lập trình viên c ++ sẽ tìm kiếm quy tắc răng cưa nghiêm ngặt và sẽ cho rằng mọi thứ được nêu ở đây cũng áp dụng cho c ++.
MikeMB

Mặt khác, tôi đồng ý rằng việc thay đổi câu hỏi là rất có vấn đề sau khi rất nhiều câu trả lời hay đã được đăng và dù sao thì vấn đề chỉ là vấn đề nhỏ.
MikeMB

1
@MikeMB: Tôi nghĩ rằng bạn sẽ thấy rằng C tập trung vào câu trả lời được chấp nhận, khiến nó không chính xác cho C ++, đã được chỉnh sửa bởi bên thứ ba. Phần đó có lẽ nên được sửa đổi một lần nữa.
Ben Voigt

Câu trả lời:


562

Một tình huống điển hình khi bạn gặp phải các vấn đề răng cưa nghiêm ngặt là khi chồng một cấu trúc (như thông điệp thiết bị / mạng) lên bộ đệm có kích thước từ của hệ thống của bạn (như con trỏ tới uint32_ts hoặc uint16_ts). Khi bạn phủ một cấu trúc lên một bộ đệm như vậy hoặc một bộ đệm lên một cấu trúc như vậy thông qua việc đúc con trỏ, bạn có thể dễ dàng vi phạm các quy tắc bí danh nghiêm ngặt.

Vì vậy, trong kiểu thiết lập này, nếu tôi muốn gửi tin nhắn đến một thứ gì đó, tôi phải có hai con trỏ không tương thích trỏ đến cùng một đoạn bộ nhớ. Sau đó, tôi có thể ngây thơ mã hóa một cái gì đó như thế này (trên một hệ thống với sizeof(int) == 2):

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // Get a 32-bit buffer from the system
    uint32_t* buff = malloc(sizeof(Msg));

    // Alias that buffer through message
    Msg* msg = (Msg*)(buff);

    // Send a bunch of messages    
    for (int i =0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendWord(buff[0]);
        SendWord(buff[1]);   
    }
}

Quy tắc bí danh nghiêm ngặt làm cho thiết lập này trở thành bất hợp pháp: hủy bỏ một con trỏ bí danh một đối tượng không phải là loại tương thích hoặc một trong các loại khác được cho phép bởi C 2011 6.5 đoạn 7 1 là hành vi không xác định. Thật không may, bạn vẫn có thể mã theo cách này, có thể nhận được một số cảnh báo, biên dịch tốt, chỉ có hành vi bất ngờ kỳ lạ khi bạn chạy mã.

(GCC có vẻ không nhất quán trong khả năng đưa ra các cảnh báo răng cưa, đôi khi đưa ra cảnh báo thân thiện và đôi khi không.)

Để xem tại sao hành vi này không được xác định, chúng ta phải suy nghĩ về những gì quy tắc bí danh nghiêm ngặt mua trình biên dịch. Về cơ bản, với quy tắc này, không cần phải suy nghĩ về việc chèn hướng dẫn để làm mới nội dung của buffmỗi lần chạy vòng lặp. Thay vào đó, khi tối ưu hóa, với một số giả định khó chịu về bí danh, nó có thể bỏ qua các hướng dẫn đó, tải buff[0]buff[1] vào các thanh ghi CPU một lần trước khi vòng lặp được chạy và tăng tốc phần thân của vòng lặp. Trước khi bí danh nghiêm ngặt được giới thiệu, trình biên dịch phải sống trong trạng thái hoang tưởng rằng nội dung buffcó thể thay đổi bất cứ lúc nào từ bất cứ nơi nào bởi bất cứ ai. Vì vậy, để có được lợi thế hiệu suất cao hơn và giả sử hầu hết mọi người không gõ kiểu con trỏ, quy tắc bí danh nghiêm ngặt đã được đưa ra.

Hãy ghi nhớ, nếu bạn nghĩ rằng ví dụ này bị chiếm đoạt, điều này thậm chí có thể xảy ra nếu bạn chuyển một bộ đệm sang một chức năng khác thực hiện việc gửi cho bạn, nếu thay vào đó bạn có.

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

Và viết lại vòng lặp trước đó của chúng tôi để tận dụng chức năng tiện lợi này

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

Trình biên dịch có thể hoặc không thể hoặc đủ thông minh để thử gửi SendMessage nội tuyến và nó có thể hoặc không thể quyết định tải hoặc không tải lại buff. Nếu SendMessagelà một phần của API khác được biên dịch riêng, thì nó có thể có hướng dẫn để tải nội dung của buff. Sau đó, một lần nữa, có thể bạn đang ở trong C ++ và đây là một số tiêu đề chỉ thực hiện mà trình biên dịch nghĩ rằng nó có thể nội tuyến. Hoặc có thể đó chỉ là thứ bạn đã viết trong tệp .c để thuận tiện cho chính bạn. Dù sao hành vi không xác định vẫn có thể xảy ra. Ngay cả khi chúng tôi biết một số điều xảy ra dưới mui xe, nó vẫn vi phạm quy tắc nên không có hành vi được xác định rõ nào được đảm bảo. Vì vậy, chỉ bằng cách gói trong một hàm có bộ đệm phân cách từ của chúng tôi không nhất thiết phải giúp đỡ.

Vì vậy, làm thế nào để tôi có được xung quanh này?

  • Sử dụng một công đoàn. Hầu hết các trình biên dịch hỗ trợ điều này mà không phàn nàn về răng cưa nghiêm ngặt. Điều này được cho phép trong C99 và được cho phép rõ ràng trong C11.

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
  • Bạn có thể vô hiệu hóa bí danh nghiêm ngặt trong trình biên dịch của mình ( f [no-] aliasing aliasing trong gcc))

  • Bạn có thể sử dụng char*cho răng cưa thay vì từ hệ thống của bạn. Các quy tắc cho phép một ngoại lệ cho char*(bao gồm signed charunsigned char). Nó luôn luôn giả định rằng char*bí danh các loại khác. Tuy nhiên, điều này sẽ không hoạt động theo cách khác: không có giả định rằng cấu trúc của bạn bí danh một bộ đệm ký tự.

Người mới bắt đầu hãy cẩn thận

Đây chỉ là một mỏ khai thác tiềm năng khi chồng hai loại lên nhau. Bạn cũng nên tìm hiểu về endianness , căn chỉnh từ và cách xử lý các vấn đề căn chỉnh thông qua các cấu trúc đóng gói chính xác.

Chú thích

1 Các loại mà C 2011 6.5 7 cho phép một giá trị truy cập là:

  • một loại tương thích với loại hiệu quả của đối tượng,
  • một phiên bản đủ điều kiện của một loại tương thích với loại hiệu quả của đối tượng,
  • một loại là loại đã ký hoặc không dấu tương ứng với loại hiệu quả của đối tượng,
  • một loại là loại đã ký hoặc không dấu tương ứng với một phiên bản đủ điều kiện của loại đối tượng hiệu quả,
  • một loại tổng hợp hoặc liên minh bao gồm một trong các loại đã nói ở trên giữa các thành viên của nó (bao gồm, đệ quy, một thành viên của liên hiệp hoặc phân nhóm), hoặc
  • một kiểu nhân vật.

16
Tôi đến sau trận chiến có vẻ như .. có thể unsigned char*được sử dụng xa hơn char*? Tôi có xu hướng sử dụng unsigned charchứ không phải charlà loại cơ bản bytevì các byte của tôi không được ký và tôi không muốn sự kỳ lạ của hành vi đã ký (đáng chú ý là bị tràn)
Matthieu M.

30
@Matthieu: Signedness không có sự khác biệt với quy tắc bí danh, vì vậy sử dụng unsigned char *là được.
Thomas Eding

22
Không phải là hành vi không xác định để đọc từ một thành viên công đoàn khác với hành vi cuối cùng được viết cho?
R. Martinho Fernandes

23
Căng, câu trả lời này là hoàn toàn ngược . Ví dụ mà nó thể hiện là bất hợp pháp là thực sự hợp pháp và ví dụ mà nó thể hiện là hợp pháp thực sự là bất hợp pháp.
R. Martinho Fernandes

7
Khai báo bộ đệm uint32_t* buff = malloc(sizeof(Msg));công đoàn của bạn và sau đó unsigned int asBuffer[sizeof(Msg)];sẽ có kích thước khác nhau và không đúng. Cuộc mallocgọi dựa vào căn chỉnh 4 byte dưới mui xe (không thực hiện) và liên kết sẽ lớn hơn 4 lần so với yêu cầu ... Tôi hiểu rằng đó là sự rõ ràng nhưng nó không gây ra lỗi gì cho tôi - ít hơn ...
vô nghĩa

233

Lời giải thích tốt nhất mà tôi đã tìm thấy là của Mike Acton, Hiểu về bí danh nghiêm ngặt . Nó tập trung một chút vào phát triển PS3, nhưng về cơ bản đó chỉ là GCC.

Từ bài viết:

"Bí danh nghiêm ngặt là một giả định, được tạo bởi trình biên dịch C (hoặc C ++), rằng các con trỏ hội thảo cho các đối tượng thuộc các loại khác nhau sẽ không bao giờ đề cập đến cùng một vị trí bộ nhớ (nghĩa là bí danh lẫn nhau.)"

Vì vậy, về cơ bản nếu bạn int*chỉ vào một số bộ nhớ có chứa một bộ nhớ intvà sau đó bạn trỏ float*đến bộ nhớ đó và sử dụng nó làm bộ nhớ floatbạn phá vỡ quy tắc. Nếu mã của bạn không tôn trọng điều này, thì trình tối ưu hóa của trình biên dịch rất có thể sẽ phá vỡ mã của bạn.

Ngoại lệ cho quy tắc là a char*, được phép trỏ đến bất kỳ loại nào.


6
Vì vậy, cách hợp quy để sử dụng hợp pháp cùng một bộ nhớ với các biến của 2 loại khác nhau là gì? hoặc tất cả mọi người chỉ sao chép?
jiggunjer

4
Trang của Mike Acton là thiếu sót. Phần "Đúc thông qua một liên minh (2)", ít nhất, là hoàn toàn sai; mã ông tuyên bố là hợp pháp là không.
davmac

11
@davmac: Các tác giả của C89 không bao giờ có ý định rằng nó nên buộc các lập trình viên nhảy qua vòng. Tôi thấy hoàn toàn kỳ quái với khái niệm rằng một quy tắc tồn tại cho mục đích tối ưu hóa duy nhất nên được diễn giải theo kiểu như vậy để yêu cầu các lập trình viên viết mã sao chép dữ liệu với hy vọng rằng trình tối ưu hóa sẽ loại bỏ mã dự phòng.
supercat

1
@cquilguy: "Không thể có công đoàn"? Thứ nhất, mục đích ban đầu / chính của các công đoàn hoàn toàn không liên quan đến răng cưa. Thứ hai, đặc tả ngôn ngữ hiện đại cho phép sử dụng các hiệp hội một cách rõ ràng. Trình biên dịch được yêu cầu thông báo rằng một liên minh được sử dụng và xử lý tình huống là một cách đặc biệt.
AnT

5
@cantlyguy: Sai. Thứ nhất, ý tưởng khái niệm ban đầu đằng sau các công đoàn là bất cứ lúc nào cũng chỉ có một đối tượng thành viên "hoạt động" trong đối tượng công đoàn nhất định, trong khi những người khác chỉ đơn giản là không tồn tại. Vì vậy, không có "đối tượng khác nhau ở cùng một địa chỉ" như bạn có thể tin. Thứ hai, vi phạm răng cưa mà mọi người đang nói đến là về việc truy cập một đối tượng như một đối tượng khác, không chỉ đơn giản là hai đối tượng có cùng địa chỉ. Miễn là không có quyền truy cập loại , không có vấn đề. Đó là ý tưởng ban đầu. Sau đó, loại hình thông qua các công đoàn đã được cho phép.
AnT

133

Đây là quy tắc răng cưa nghiêm ngặt, được tìm thấy trong phần 3.10 của tiêu chuẩn C ++ 03 (các câu trả lời khác cung cấp giải thích tốt, nhưng không có quy tắc nào cung cấp chính quy tắc này):

Nếu một chương trình cố gắng truy cập giá trị được lưu trữ của một đối tượng thông qua một giá trị khác với một trong các loại sau đây thì hành vi không được xác định:

  • loại động của đối tượng,
  • một phiên bản đủ điều kiện cv của loại động của đối tượng,
  • một loại là loại đã ký hoặc không dấu tương ứng với loại động của đối tượng,
  • một loại là loại đã ký hoặc không dấu tương ứng với phiên bản đủ điều kiện cv của loại động của đối tượng,
  • một loại tổng hợp hoặc liên minh bao gồm một trong các loại đã nói ở trên trong số các thành viên của nó (bao gồm, đệ quy, một thành viên của một phân nhóm hoặc liên kết có chứa),
  • một loại là loại lớp cơ sở (có thể đủ điều kiện cv) của loại động của đối tượng,
  • một charhoặc unsigned charloại.

Từ ngữ C ++ 11C ++ 14 (những thay đổi được nhấn mạnh):

Nếu một nỗ lực chương trình để truy cập giá trị được lưu trữ của một đối tượng thông qua một glvalue của khác hơn là một trong các loại sau đây hành vi này là không xác định:

  • loại động của đối tượng,
  • một phiên bản đủ điều kiện cv của loại động của đối tượng,
  • một loại tương tự (như được định nghĩa trong 4.4) với loại động của đối tượng,
  • một loại là loại đã ký hoặc không dấu tương ứng với loại động của đối tượng,
  • một loại là loại đã ký hoặc không dấu tương ứng với phiên bản đủ điều kiện cv của loại động của đối tượng,
  • một loại tổng hợp hoặc kết hợp bao gồm một trong các loại đã nói ở trên trong số các thành phần của nó hoặc các thành viên dữ liệu không tĩnh (bao gồm, đệ quy, một thành phần hoặc thành viên dữ liệu không tĩnh của một phân nhóm hoặc liên kết có chứa),
  • một loại là loại lớp cơ sở (có thể đủ điều kiện cv) của loại động của đối tượng,
  • một charhoặc unsigned charloại.

Hai thay đổi là nhỏ: glvalue thay vì lvalue và làm rõ trường hợp tổng hợp / liên minh.

Thay đổi thứ ba làm cho một sự bảo đảm mạnh mẽ hơn (nới lỏng quy tắc bí danh mạnh mẽ): Khái niệm mới về các loại tương tự hiện an toàn với bí danh.


Ngoài ra, từ ngữ C (C99; ISO / IEC 9899: 1999 6.5 / 7; từ ngữ chính xác được sử dụng trong ISO / IEC 9899: 2011 §6.5 7):

Một đối tượng sẽ có giá trị được lưu trữ chỉ được truy cập bằng biểu thức lvalue có một trong các loại sau 73) hoặc 88) :

  • một loại tương thích với loại hiệu quả của đối tượng,
  • phiên bản quali của một loại tương thích với loại hiệu quả của đối tượng,
  • một loại là loại đã ký hoặc không dấu tương ứng với loại hiệu quả của đối tượng,
  • một loại là loại đã ký hoặc không dấu tương ứng với phiên bản quali của loại đối tượng hiệu quả,
  • một loại tổng hợp hoặc liên minh bao gồm một trong các loại đã nói ở trên giữa các thành viên của nó (bao gồm, đệ quy, một thành viên của liên hiệp hoặc phân nhóm), hoặc
  • một kiểu nhân vật.

73) hoặc 88) Mục đích của danh sách này là chỉ định những trường hợp trong đó một đối tượng có thể hoặc không được đặt bí danh.


7
Ben, như mọi người thường được hướng dẫn ở đây, tôi cũng đã cho phép bản thân thêm tham chiếu vào tiêu chuẩn C, vì mục đích hoàn chỉnh.
Kos

1
Hãy xem C89 Rationale cs.technion.ac.il/users/yechiel/CS/C++draft/rationale.pdf phần 3.3 nói về nó.
tổ chức1

2
Nếu một người có giá trị của loại cấu trúc, lấy địa chỉ của thành viên và chuyển nó đến một hàm sử dụng nó làm con trỏ cho loại thành viên, thì điều đó có thể được coi là truy cập vào một đối tượng của loại thành viên (hợp pháp), hoặc một đối tượng của loại cấu trúc (bị cấm)? Rất nhiều mã cho rằng việc truy cập các cấu trúc theo kiểu như vậy là hợp pháp và tôi nghĩ rằng nhiều người sẽ vặn vẹo theo một quy tắc được hiểu là cấm các hành động đó, nhưng không rõ quy tắc chính xác là gì. Hơn nữa, các công đoàn và cấu trúc được đối xử như nhau, nhưng các quy tắc hợp lý cho mỗi quy tắc nên khác nhau.
supercat

2
@supercat: Cách quy tắc cho các cấu trúc được diễn đạt, truy cập thực tế luôn luôn là loại nguyên thủy. Sau đó, truy cập thông qua tham chiếu đến loại nguyên thủy là hợp pháp vì các loại khớp và truy cập qua tham chiếu đến loại cấu trúc chứa là hợp pháp vì được phép đặc biệt.
Ben Voigt

2
@BenVoigt: Tôi không nghĩ trình tự ban đầu phổ biến hoạt động trừ khi truy cập được thực hiện thông qua liên minh. Xem goo.gl/HGOyoK để xem gcc đang làm gì. Nếu việc truy cập một giá trị của loại kết hợp thông qua một giá trị của loại thành viên (không sử dụng toán tử truy cập thành viên công đoàn) là hợp pháp, thì wow(&u->s1,&u->s2)sẽ cần phải hợp pháp ngay cả khi một con trỏ được sử dụng để sửa đổi uvà điều đó sẽ phủ nhận hầu hết các tối ưu hóa rằng quy tắc răng cưa được thiết kế để tạo điều kiện.
supercat

81

Ghi chú

Điều này được trích từ "Quy tắc bí danh nghiêm ngặt của tôi là gì và tại sao chúng ta quan tâm?" hãy viết ra giấy.

Bí danh nghiêm ngặt là gì?

Trong bí danh C và C ++ có liên quan đến loại biểu thức nào chúng ta được phép truy cập các giá trị được lưu trữ thông qua. Trong cả C và C ++, tiêu chuẩn chỉ định loại biểu thức nào được phép đặt bí danh cho loại nào. Trình biên dịch và trình tối ưu hóa được phép giả định rằng chúng tôi tuân thủ nghiêm ngặt các quy tắc răng cưa, do đó thuật ngữ quy tắc răng cưa nghiêm ngặt . Nếu chúng tôi cố gắng truy cập một giá trị bằng cách sử dụng một loại không được phép, nó được phân loại là hành vi không xác định ( UB ). Khi chúng tôi có hành vi không xác định, tất cả các cược đã tắt, kết quả của chương trình của chúng tôi không còn đáng tin cậy nữa.

Thật không may với các vi phạm bí danh nghiêm ngặt, chúng tôi sẽ thường nhận được kết quả mà chúng tôi mong đợi, để lại khả năng phiên bản tương lai của trình biên dịch với tối ưu hóa mới sẽ phá vỡ mã mà chúng tôi cho là hợp lệ. Điều này là không mong muốn và nó là một mục tiêu đáng giá để hiểu các quy tắc bí danh nghiêm ngặt và làm thế nào để tránh vi phạm chúng.

Để hiểu thêm về lý do tại sao chúng tôi quan tâm, chúng tôi sẽ thảo luận về các vấn đề phát sinh khi vi phạm các quy tắc răng cưa nghiêm ngặt, loại xảo quyệt vì các kỹ thuật phổ biến được sử dụng trong loại picky thường vi phạm các quy tắc bí danh nghiêm ngặt và cách gõ chữ đúng.

Ví dụ sơ bộ

Chúng ta hãy xem xét một số ví dụ, sau đó chúng ta có thể nói về chính xác những gì các tiêu chuẩn nói, kiểm tra một số ví dụ khác và sau đó xem làm thế nào để tránh bí danh nghiêm ngặt và bắt vi phạm mà chúng ta đã bỏ lỡ. Đây là một ví dụ không đáng ngạc nhiên ( ví dụ trực tiếp ):

int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

Chúng ta có một int * trỏ đến bộ nhớ bị chiếm bởi một int và đây là một bí danh hợp lệ. Trình tối ưu hóa phải cho rằng các bài tập thông qua ip có thể cập nhật giá trị chiếm dụng bởi x .

Ví dụ tiếp theo cho thấy răng cưa dẫn đến hành vi không xác định ( ví dụ trực tiếp ):

int foo( float *f, int *i ) { 
    *i = 1;               
    *f = 0.f;            

   return *i;
}

int main() {
    int x = 0;

    std::cout << x << "\n";   // Expect 0
    x = foo(reinterpret_cast<float*>(&x), &x);
    std::cout << x << "\n";   // Expect 0?
}

Trong hàm foo, chúng ta lấy một int *float * , trong ví dụ này, chúng ta gọi foo và đặt cả hai tham số để trỏ đến cùng một vị trí bộ nhớ mà trong ví dụ này chứa một int . Lưu ý, reinterpret_cast đang báo cho trình biên dịch xử lý biểu thức như thể nó có kiểu được chỉ định bởi tham số mẫu của nó. Trong trường hợp này, chúng tôi đang bảo nó xử lý biểu thức & x như thể nó có kiểu float * . Chúng tôi có thể ngây thơ mong đợi kết quả của cout thứ hai là 0 nhưng với tối ưu hóa được kích hoạt bằng cách sử dụng -O2 cả gcc và clang tạo ra kết quả như sau:

0
1

Điều này có thể không được mong đợi nhưng hoàn toàn hợp lệ vì chúng tôi đã gọi hành vi không xác định. Một float không thể hợp lệ bí danh một đối tượng int . Do đó, trình tối ưu hóa có thể giả sử hằng số 1 được lưu trữ khi hội thảo i sẽ là giá trị trả về do một cửa hàng qua f không thể ảnh hưởng hợp lệ đến một đối tượng int . Việc cắm mã trong Compiler Explorer cho thấy đây chính xác là những gì đang xảy ra ( ví dụ trực tiếp ):

foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1  
mov dword ptr [rdi], 0
mov eax, 1                       
ret

Trình tối ưu hóa sử dụng Phân tích bí danh dựa trên loại (TBAA) giả định 1 sẽ được trả về và trực tiếp di chuyển giá trị không đổi vào thanh ghi eax mang giá trị trả về. TBAA sử dụng các quy tắc ngôn ngữ về loại nào được phép đặt bí danh để tối ưu hóa tải và lưu trữ. Trong trường hợp này, TBAA biết rằng một float không thể bí danh và int và tối ưu hóa tải của i .

Bây giờ, đến Quy tắc

Chính xác thì tiêu chuẩn nói chúng ta được phép và không được phép làm gì? Ngôn ngữ tiêu chuẩn không đơn giản, vì vậy đối với mỗi mục tôi sẽ cố gắng cung cấp các ví dụ mã thể hiện ý nghĩa.

Tiêu chuẩn C11 nói lên điều gì?

Các C11 tiêu chuẩn nói sau đây trong phần 6,5 Expressions đoạn 7 :

Một đối tượng sẽ có giá trị được lưu trữ chỉ được truy cập bằng biểu thức lvalue có một trong các loại sau: 88) - một loại tương thích với loại hiệu quả của đối tượng,

int x = 1;
int *p = &x;   
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int

- phiên bản đủ điều kiện của loại tương thích với loại đối tượng hiệu quả,

int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int

- một loại là loại đã ký hoặc không dấu tương ứng với loại hiệu quả của đối tượng,

int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to 
                     // the effective type of the object

gcc / clang có một phần mở rộngcũng cho phép gán int * không dấu cho int * mặc dù chúng không phải là loại tương thích.

- một loại là loại đã ký hoặc không dấu tương ứng với một phiên bản đủ điều kiện của loại đối tượng hiệu quả,

int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type 
                     // that corresponds with to a qualified verison of the effective type of the object

- một loại tổng hợp hoặc liên minh bao gồm một trong các loại đã nói ở trên trong số các thành viên của nó (bao gồm, đệ quy, một thành viên của một tập hợp phụ hoặc có liên kết), hoặc

struct foo {
  int x;
};

void foobar( struct foo *fp, int *ip );  // struct foo is an aggregate that includes int among its members so it can
                                         // can alias with *ip

foo f;
foobar( &f, &f.x );

- một kiểu nhân vật.

int x = 65;
char *p = (char *)&x;
printf("%c\n", *p );  // *p gives us an lvalue expression of type char which is a character type.
                      // The results are not portable due to endianness issues.

Tiêu chuẩn Dự thảo C ++ 17 nói gì

Tiêu chuẩn dự thảo C ++ 17 trong phần [basic.lval] đoạn 11 nói:

Nếu một chương trình cố gắng truy cập giá trị được lưu trữ của một đối tượng thông qua một giá trị khác với một trong các loại sau đây thì hành vi không được xác định: 63 (11.1) - loại động của đối tượng,

void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0};        // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n";        // *ip gives us a glvalue expression of type int which matches the dynamic type 
                                  // of the allocated object

(11.2) - phiên bản đủ điều kiện cv của loại động của đối tượng,

int x = 1;
const int *cip = &x;
std::cout << *cip << "\n";  // *cip gives us a glvalue expression of type const int which is a cv-qualified 
                            // version of the dynamic type of x

(11.3) - một loại tương tự (như được định nghĩa trong 7.5) với loại động của đối tượng,

(11.4) - một loại là loại đã ký hoặc không dấu tương ứng với loại động của đối tượng,

// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
  si = 1;
  ui = 2;

  return si;
}

(11,5) - một loại là loại đã ký hoặc không dấu tương ứng với phiên bản đủ điều kiện cv của loại động của đối tượng,

signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing

(11.6) - một loại tổng hợp hoặc kết hợp bao gồm một trong các loại đã nói ở trên trong số các thành phần hoặc thành viên dữ liệu không phải là dữ liệu (bao gồm, đệ quy, một thành phần hoặc thành viên dữ liệu không tĩnh của liên kết gộp hoặc chứa)

struct foo {
 int x;
};

// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
 fp.x = 1;
 ip = 2;

 return fp.x;
}

foo f; 
foobar( f, f.x ); 

(11.7) - một loại là loại lớp cơ sở (có thể đủ điều kiện cv) của loại động của đối tượng,

struct foo { int x ; };

struct bar : public foo {};

int foobar( foo &f, bar &b ) {
  f.x = 1;
  b.x = 2;

  return f.x;
}

(11.8) - một kiểu char, unsign char hoặc std :: byte.

int foo( std::byte &b, uint32_t &ui ) {
  b = static_cast<std::byte>('a');
  ui = 0xFFFFFFFF;                   

  return std::to_integer<int>( b );  // b gives us a glvalue expression of type std::byte which can alias
                                     // an object of type uint32_t
}

Đáng chú ý char đã ký không được bao gồm trong danh sách trên, đây là một sự khác biệt đáng chú ý từ C mà nói một loại ký tự .

Kiểu trừng phạt là gì

Chúng tôi đã đi đến điểm này và chúng tôi có thể tự hỏi, tại sao chúng tôi muốn bí danh cho? Câu trả lời thường là gõ chữ , thường các phương thức được sử dụng vi phạm các quy tắc răng cưa nghiêm ngặt.

Đôi khi chúng tôi muốn phá vỡ hệ thống loại và giải thích một đối tượng là một loại khác. Điều này được gọi là loại pucky , để diễn giải lại một đoạn bộ nhớ như một loại khác. Kiểu pucky rất hữu ích cho các tác vụ muốn truy cập vào biểu diễn cơ bản của một đối tượng để xem, vận chuyển hoặc thao tác. Các lĩnh vực điển hình mà chúng tôi thấy loại pucky đang được sử dụng là trình biên dịch, tuần tự hóa, mã mạng, v.v.

Theo truyền thống, điều này đã được thực hiện bằng cách lấy địa chỉ của đối tượng, chuyển nó thành một con trỏ của loại mà chúng ta muốn diễn giải lại nó và sau đó truy cập giá trị, hay nói cách khác bằng cách đặt bí danh. Ví dụ:

int x =  1 ;

// In C
float *fp = (float*)&x ;  // Not a valid aliasing

// In C++
float *fp = reinterpret_cast<float*>(&x) ;  // Not a valid aliasing

printf( "%f\n", *fp ) ;

Như chúng ta đã thấy trước đây, đây không phải là một bí danh hợp lệ, vì vậy chúng ta đang gọi hành vi không xác định. Nhưng các trình biên dịch truyền thống đã không tận dụng các quy tắc bí danh nghiêm ngặt và loại mã này thường chỉ hoạt động, các nhà phát triển đã không may làm quen với việc làm theo cách này. Một phương pháp thay thế phổ biến cho loại picky là thông qua các hiệp hội, có giá trị trong C nhưng hành vi không xác định trong C ++ ( xem ví dụ trực tiếp ):

union u1
{
  int n;
  float f;
} ;

union u1 u;
u.f = 1.0f;

printf( "%d\n”, u.n );  // UB in C++ n is not the active member

Điều này không hợp lệ trong C ++ và một số người coi mục đích của các công đoàn chỉ là để thực hiện các loại biến thể và cảm thấy việc sử dụng các công đoàn cho loại picky là một sự lạm dụng.

Làm thế nào để chúng ta gõ Pun chính xác?

Phương pháp tiêu chuẩn để loại pucky trong cả C và C ++ là memcpy . Điều này có vẻ hơi nặng tay nhưng trình tối ưu hóa sẽ nhận ra việc sử dụng memcpy để loại pucky và tối ưu hóa nó đi và tạo ra một đăng ký để đăng ký di chuyển. Ví dụ: nếu chúng ta biết int64_t có cùng kích thước với double :

static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message

chúng ta có thể sử dụng memcpy :

void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...

Ở mức tối ưu hóa đủ, bất kỳ trình biên dịch hiện đại phong nha nào cũng tạo ra mã giống hệt với phương thức reinterpret_cast hoặc phương thức hợp nhất đã đề cập trước đó để loại picky . Kiểm tra mã được tạo, chúng tôi thấy nó chỉ sử dụng đăng ký Mov ( Ví dụ trình biên dịch trình biên dịch trực tiếp ).

C ++ 20 và bit_cast

Trong C ++ 20, chúng tôi có thể nhận được bit_cast ( triển khai có sẵn trong liên kết từ đề xuất ), điều này mang lại một cách đơn giản và an toàn để chơi chữ cũng như có thể sử dụng được trong ngữ cảnh constexpr.

Sau đây là một ví dụ về cách sử dụng bit_cast để gõ chữ int unsced để nổi , ( xem trực tiếp ):

std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)

Trong trường hợp các loại ToFrom không có cùng kích thước, nó yêu cầu chúng ta sử dụng một cấu trúc trung gian15. Chúng ta sẽ sử dụng một cấu trúc chứa một mảng ký tự sizeof (unsign int) ( giả sử 4 byte unsign int ) là kiểu Fromunsign int như kiểu To .:

struct uint_chars {
 unsigned char arr[sizeof( unsigned int )] = {} ;  // Assume sizeof( unsigned int ) == 4
};

// Assume len is a multiple of 4 
int bar( unsigned char *p, size_t len ) {
 int result = 0;

 for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
   uint_chars f;
   std::memcpy( f.arr, &p[index], sizeof(unsigned int));
   unsigned int result = bit_cast<unsigned int>(f);

   result += foo( result );
 }

 return result ;
}

Thật không may là chúng ta cần loại trung gian này nhưng đó là ràng buộc hiện tại của bit_cast .

Bắt vi phạm bí danh nghiêm ngặt

Chúng tôi không có nhiều công cụ tốt để bắt răng cưa nghiêm ngặt trong C ++, các công cụ chúng tôi có sẽ bắt được một số trường hợp vi phạm bí danh nghiêm ngặt và một số trường hợp tải và lưu trữ sai.

gcc sử dụng hàm răng cưa -fstrict-aliasing-Wstrict-aliasing có thể bắt được một số trường hợp mặc dù không phải không có dương / âm sai. Ví dụ: các trường hợp sau sẽ tạo cảnh báo trong gcc ( xem trực tiếp ):

int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught 
               // it was being accessed w/ an indeterminate value below

printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));

mặc dù nó sẽ không bắt được trường hợp bổ sung này ( xem trực tiếp ):

int *p;

p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));

Mặc dù clang cho phép những lá cờ này nhưng dường như nó không thực sự cảnh báo.

Một công cụ khác mà chúng tôi có sẵn cho chúng tôi là ASan có thể bắt các tải và cửa hàng bị sai lệch. Mặc dù đây không phải là những vi phạm răng cưa trực tiếp nghiêm ngặt nhưng chúng là kết quả chung của các vi phạm răng cưa nghiêm ngặt. Ví dụ: các trường hợp sau sẽ tạo ra lỗi thời gian chạy khi được tạo bằng clang bằng cách sử dụng -fsanitize = address

int *x = new int[2];               // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6);     // regardless of alignment of x this will not be an aligned address
*u = 1;                            // Access to range [6-9]
printf( "%d\n", *u );              // Access to range [6-9]

Công cụ cuối cùng tôi sẽ giới thiệu là C ++ cụ thể và không hoàn toàn là một công cụ mà là thực hành mã hóa, không cho phép các kiểu phôi C. Cả gcc và clang sẽ tạo ra chẩn đoán cho các kiểu phôi C bằng cách sử dụng -Wold-style-cast . Điều này sẽ buộc mọi kiểu chơi chữ không xác định sử dụng reinterpret_cast, nói chung reinterpret_cast phải là một cờ để xem xét mã gần hơn. Nó cũng dễ dàng hơn để tìm kiếm cơ sở mã của bạn cho reinterpret_cast để thực hiện kiểm toán.

Đối với C, chúng tôi có tất cả các công cụ đã được trình bày và chúng tôi cũng có trình thông dịch tis, một bộ phân tích tĩnh phân tích triệt để một chương trình cho một tập hợp con lớn của ngôn ngữ C. Đưa ra một câu C của ví dụ trước đó trong đó việc sử dụng -fstrict-aliasing bỏ lỡ một trường hợp ( xem trực tiếp )

int a = 1;
short j;
float f = 1.0 ;

printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));

int *p; 

p=&a;
printf("%i\n", j = *((short*)p));

tis-interpeter có thể bắt được cả ba, ví dụ sau đây gọi tis-kernal là tis-phiên dịch (đầu ra được chỉnh sửa cho ngắn gọn):

./bin/tis-kernel -sa example1.c 
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
              rules by accessing a cell with effective type int.
...

example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
              accessing a cell with effective type float.
              Callstack: main
...

example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
              accessing a cell with effective type int.

Cuối cùng là TySan hiện đang được phát triển. Trình khử trùng này thêm thông tin kiểm tra loại trong phân đoạn bộ nhớ bóng và kiểm tra truy cập để xem liệu chúng có vi phạm quy tắc răng cưa hay không. Công cụ có khả năng có thể bắt được tất cả các vi phạm răng cưa nhưng có thể có chi phí hoạt động lớn.


Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
Bhargav Rao

3
Nếu tôi có thể, +10, được viết và giải thích tốt, cũng từ cả hai phía, người viết trình biên dịch và lập trình viên ... thì chỉ trích duy nhất: Thật tốt khi có các ví dụ phản biện ở trên, để xem những gì bị cấm theo tiêu chuẩn, không rõ ràng loại :-)
Gabriel

2
Câu trả lời rất hay. Tôi chỉ tiếc rằng các ví dụ ban đầu được đưa ra trong C ++, điều này gây khó khăn cho những người như tôi, những người chỉ biết hoặc quan tâm đến C và không biết những gì reinterpret_castcó thể làm hoặc coutcó nghĩa là gì . (Bạn hoàn toàn có thể đề cập đến C ++ nhưng câu hỏi ban đầu là về C và IIUC, những ví dụ này có thể được viết một cách hợp lệ bằng C.)
Gro-Tsen

Về kiểu chơi chữ: vì vậy nếu tôi viết một mảng của một số loại X vào tệp, sau đó đọc từ tệp đó mảng này vào bộ nhớ được chỉ bằng void *, thì tôi chuyển con trỏ đó sang loại dữ liệu thực để sử dụng nó - đó là hành vi không xác định?
Michael IV

44

Bí danh nghiêm ngặt không chỉ đề cập đến con trỏ, nó cũng ảnh hưởng đến các tài liệu tham khảo, tôi đã viết một bài báo về nó cho wiki nhà phát triển thúc đẩy và nó đã được đón nhận đến mức tôi đã biến nó thành một trang trên trang web tư vấn của mình. Nó giải thích hoàn toàn nó là gì, tại sao nó làm mọi người bối rối và phải làm gì với nó. Giấy trắng răng cưa nghiêm ngặt . Cụ thể, nó giải thích tại sao các công đoàn là hành vi rủi ro cho C ++ và tại sao sử dụng memcpy là bản sửa lỗi duy nhất có thể di chuyển trên cả C và C ++. Hy vọng điều này là hữu ích.


3
" Bí danh nghiêm ngặt không chỉ đề cập đến con trỏ, nó cũng ảnh hưởng đến các tham chiếu " Trên thực tế, nó đề cập đến giá trị . " Sử dụng memcpy là bản sửa lỗi di động duy nhất " Nghe!
tò mò

5
Giấy tốt. Tôi đưa ra: (1) bí danh này là 'phản ứng' là một phản ứng thái quá đối với lập trình xấu - cố gắng bảo vệ lập trình viên xấu khỏi những thói quen xấu của anh ấy / cô ấy. Nếu lập trình viên có thói quen tốt thì việc khử răng cưa này chỉ gây phiền toái và kiểm tra có thể được tắt một cách an toàn. (2) Tối ưu hóa phía trình biên dịch chỉ nên được thực hiện trong các trường hợp nổi tiếng và khi nghi ngờ tuân thủ nghiêm ngặt mã nguồn; buộc các lập trình viên phải viết mã để phục vụ cho các đặc điểm riêng của nhà soạn nhạc, đơn giản là, đã sai. Thậm chí tệ hơn để làm cho nó một phần của tiêu chuẩn.
slashmais 2/2/2015

4
@slashmais (1) " là phản ứng thái quá đối với lập trình xấu " Vô nghĩa. Đó là sự từ chối những thói quen xấu. Bạn làm điều đó? Bạn trả giá: không đảm bảo cho bạn! (2) Những trường hợp nổi tiếng? Những cái nào? Quy tắc răng cưa nghiêm ngặt nên được "biết đến"!
tò mò

5
@cquilguy: Đã xóa được một vài điểm nhầm lẫn, rõ ràng ngôn ngữ C với các quy tắc răng cưa khiến các chương trình không thể thực hiện các nhóm bộ nhớ không xác định loại. Một số loại chương trình có thể nhận được bằng malloc / miễn phí, nhưng một số loại khác cần logic quản lý bộ nhớ phù hợp hơn với các nhiệm vụ trong tay. Tôi tự hỏi tại sao lý do C89 sử dụng một ví dụ nhàu nát như vậy về lý do của quy tắc răng cưa, vì ví dụ của họ làm cho có vẻ như quy tắc sẽ không gây khó khăn lớn trong việc thực hiện bất kỳ nhiệm vụ hợp lý nào.
supercat

5
@cquilguy, hầu hết các bộ biên dịch ngoài đó đều bao gồm -fstrict-aliasing như mặc định trên -O3 và hợp đồng ẩn này bị ép buộc đối với những người dùng chưa bao giờ nghe về TBAA và viết mã như cách lập trình viên hệ thống có thể. Tôi không có ý nói không hợp lý với các lập trình viên hệ thống, nhưng loại tối ưu hóa này nên được đặt ngoài phạm vi lựa chọn mặc định của -O3 và nên là tối ưu hóa tùy chọn cho những người biết TBAA là gì. Thật không vui khi nhìn vào 'lỗi' của trình biên dịch hóa ra là mã người dùng vi phạm TBAA, đặc biệt là theo dõi vi phạm mức nguồn trong mã người dùng.
kchoi

34

Như phần phụ lục cho những gì Doug T. đã viết, đây là một trường hợp thử nghiệm đơn giản có thể kích hoạt nó với gcc:

kiểm tra

#include <stdio.h>

void check(short *h,long *k)
{
    *h=5;
    *k=6;
    if (*h == 5)
        printf("strict aliasing problem\n");
}

int main(void)
{
    long      k[1];
    check((short *)k,k);
    return 0;
}

Biên dịch với gcc -O2 -o check check.c. Thông thường (với hầu hết các phiên bản gcc tôi đã thử), điều này dẫn đến "vấn đề răng cưa nghiêm ngặt", bởi vì trình biên dịch giả định rằng "h" không thể có cùng địa chỉ với "k" trong chức năng "kiểm tra". Do đó, trình biên dịch tối ưu hóa if (*h == 5)đi và luôn gọi printf.

Đối với những người quan tâm ở đây là mã trình biên dịch x64, được sản xuất bởi gcc 4.6.3, chạy trên Ubuntu 12.04.2 cho x64:

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

Vì vậy, điều kiện if hoàn toàn biến mất khỏi mã trình biên dịch chương trình.


nếu bạn thêm một đoạn ngắn thứ hai * j để kiểm tra () và sử dụng nó (* j = 7) thì tối ưu hóa sẽ không xuất hiện vì ggc không không nếu h và j không thực tế đến cùng một giá trị. có tối ưu hóa là thực sự thông minh.
philippe lhardy 30/12/13

2
Để làm cho mọi thứ trở nên thú vị hơn, hãy sử dụng các con trỏ tới các loại không tương thích nhưng có cùng kích thước và đại diện (trên một số hệ thống đúng như ví dụ long long*int64_t*). Người ta có thể mong đợi rằng một trình biên dịch lành mạnh sẽ nhận ra rằng a long long*int64_t*có thể truy cập cùng một bộ lưu trữ nếu chúng được lưu trữ giống hệt nhau, nhưng cách xử lý như vậy không còn hợp thời nữa.
supercat

Grr ... x64 là một quy ước của Microsoft. Sử dụng amd64 hoặc x86_64 thay thế.
SS Anne

Grr ... x64 là một quy ước của Microsoft. Sử dụng amd64 hoặc x86_64 thay thế.
SS Anne

17

Gõ picky thông qua các con trỏ phôi (trái ngược với việc sử dụng một liên minh) là một ví dụ chính về việc phá vỡ bí danh nghiêm ngặt.


1
Xem câu trả lời của tôi ở đây để biết các trích dẫn có liên quan, đặc biệt là các chú thích nhưng gõ lách qua các hiệp hội luôn được cho phép trong C mặc dù lúc đầu nó được sử dụng rất kém. Bạn của tôi muốn làm rõ câu trả lời của bạn.
Shafik Yaghmour

@ShafikYaghmour: C89 rõ ràng cho phép những người thực hiện lựa chọn các trường hợp trong đó họ sẽ hoặc không nhận ra một cách hữu ích kiểu đánh cắp thông qua các hiệp hội. Ví dụ, một triển khai có thể chỉ định rằng để ghi vào một loại theo sau là đọc một loại khác được công nhận là loại xảo quyệt, nếu lập trình viên đã thực hiện một trong những điều sau đây giữa ghi và đọc : (1) đánh giá một giá trị có chứa loại liên minh [lấy địa chỉ của một thành viên sẽ đủ điều kiện, nếu được thực hiện đúng thời điểm trong chuỗi]; (2) chuyển đổi một con trỏ thành một loại thành một con trỏ sang loại khác và truy cập thông qua ptr đó.
supercat

@ShafikYaghmour: Việc triển khai cũng có thể chỉ định, ví dụ, loại picky giữa các giá trị số nguyên và dấu phẩy động sẽ chỉ hoạt động đáng tin cậy nếu mã thực thi một lệnh fpsync()giữa viết dưới dạng fp và đọc dưới dạng int hoặc ngược lại [trên các triển khai với các đường dẫn và bộ đệm số nguyên và FPU riêng biệt , một lệnh như vậy có thể tốn kém, nhưng không tốn kém như việc trình biên dịch thực hiện đồng bộ hóa như vậy trên mỗi lần truy cập liên minh]. Hoặc việc triển khai có thể chỉ định rằng giá trị kết quả sẽ không bao giờ có thể sử dụng được ngoại trừ trong các trường hợp sử dụng Chuỗi ban đầu chung.
supercat

@ShafikYaghmour: Theo C89, việc triển khai có thể cấm hầu hết các hình thức loại hình, bao gồm cả thông qua các hiệp hội, nhưng sự tương đương giữa con trỏ với công đoàn và con trỏ đối với các thành viên của họ ngụ ý rằng việc loại bỏ được phép trong các triển khai không được phép rõ ràng .
supercat

17

Theo lý do C89, các tác giả của Tiêu chuẩn không muốn yêu cầu các trình biên dịch đưa ra mã như:

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

nên được yêu cầu tải lại giá trị xgiữa câu lệnh gán và trả về để cho phép khả năng pcó thể trỏ đến xvà do đó việc gán *pcó thể thay đổi giá trị của x. Quan điểm cho rằng một trình biên dịch nên có quyền cho rằng sẽ không có bí danh trong các tình huống như trên là không gây tranh cãi.

Thật không may, các tác giả của C89 đã viết quy tắc của họ theo cách mà nếu đọc theo nghĩa đen, sẽ làm cho ngay cả hàm sau gọi ra Hành vi không xác định:

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

bởi vì nó sử dụng một giá trị loại intđể truy cập một đối tượng của loại struct Sintkhông nằm trong số các loại có thể được sử dụng khi truy cập a struct S. Bởi vì thật phi lý khi coi tất cả việc sử dụng các thành viên không thuộc loại cấu trúc của các cấu trúc và công đoàn là Hành vi không xác định, nên hầu như mọi người đều nhận ra rằng có ít nhất một số trường hợp có thể sử dụng một loại giá trị để truy cập vào một đối tượng thuộc loại khác . Thật không may, Ủy ban Tiêu chuẩn C đã không xác định được những trường hợp đó là gì.

Phần lớn vấn đề là kết quả của Báo cáo lỗi # 028, đã hỏi về hành vi của một chương trình như:

int test(int *ip, double *dp)
{
  *ip = 1;
  *dp = 1.23;
  return *ip;
}
int test2(void)
{
  union U { int i; double d; } u;
  return test(&u.i, &u.d);
}

Báo cáo khiếm khuyết # 28 nói rằng chương trình gọi Hành vi không xác định vì hành động viết một thành viên công đoàn loại "kép" và đọc một loại "int" gọi hành vi Xác định thực hiện. Lý luận như vậy là vô nghĩa, nhưng tạo cơ sở cho các quy tắc Loại hiệu quả, điều này làm phức tạp ngôn ngữ trong khi không làm gì để giải quyết vấn đề ban đầu.

Cách tốt nhất để giải quyết vấn đề ban đầu có lẽ là xử lý chú thích về mục đích của quy tắc như thể nó là quy phạm và làm cho quy tắc không thể thực thi được trừ khi trong trường hợp thực sự liên quan đến truy cập xung đột bằng cách sử dụng bí danh. Đưa ra một cái gì đó như:

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   s.x = 1;
   p = &s.x;
   inc_int(p);
   return s.x;
 }

Không có xung đột bên trong inc_intbởi vì tất cả các truy cập vào bộ lưu trữ được truy cập *pđều được thực hiện với một giá trị loại intvà không có xung đột testpcó nguồn gốc rõ ràng từ một struct S, và vào lần tiếp theo sđược sử dụng, tất cả các quyền truy cập vào bộ lưu trữ đó sẽ được thực hiện thông qua psẽ đã xảy ra.

Nếu mã được thay đổi một chút ...

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   p = &s.x;
   s.x = 1;  //  !!*!!
   *p += 1;
   return s.x;
 }

Ở đây, có một xung đột răng cưa giữa pvà quyền truy cập s.xvào dòng được đánh dấu bởi vì tại thời điểm đó trong thực thi, một tham chiếu khác tồn tại sẽ được sử dụng để truy cập vào cùng một bộ lưu trữ .

Báo cáo khiếm khuyết 028 cho biết ví dụ ban đầu đã gọi UB vì sự chồng chéo giữa việc tạo và sử dụng hai con trỏ, điều đó sẽ làm cho mọi thứ rõ ràng hơn rất nhiều mà không cần phải thêm "Loại hiệu quả" hoặc sự phức tạp khác.


Chà, thật thú vị khi đọc một đề xuất các loại ít nhiều là "những gì mà ủy ban tiêu chuẩn có thể làm" đã đạt được mục tiêu của họ mà không đưa ra nhiều sự phức tạp.
jrh

1
@jrh: Tôi nghĩ nó sẽ khá đơn giản. Nhận ra rằng 1. Để răng cưa xảy ra trong khi thực hiện một chức năng hoặc vòng lặp cụ thể, hai con trỏ hoặc giá trị khác nhau phải được sử dụng trong quá trình thực thi đó để xử lý cùng một bộ lưu trữ trong fashon xung đột; 2. Nhận ra rằng trong các bối cảnh nơi một con trỏ hoặc giá trị mới có nguồn gốc rõ ràng từ một con trỏ khác, quyền truy cập vào cái thứ hai là quyền truy cập vào cái đầu tiên; 3. Nhận ra rằng quy tắc này không có ý định áp dụng trong các trường hợp không thực sự liên quan đến răng cưa.
supercat

1
Các trường hợp chính xác trong đó trình biên dịch nhận ra giá trị xuất phát mới có thể là vấn đề Chất lượng thực hiện, nhưng bất kỳ trình biên dịch từ xa nào cũng có thể nhận ra các biểu mẫu mà gcc và clang cố tình bỏ qua.
supercat

11

Sau khi đọc nhiều câu trả lời, tôi cảm thấy cần phải thêm một cái gì đó:

Bí danh nghiêm ngặt (mà tôi sẽ mô tả một chút) rất quan trọng vì :

  1. Truy cập bộ nhớ có thể tốn kém (hiệu năng khôn ngoan), đó là lý do tại sao dữ liệu được thao tác trong các thanh ghi CPU trước khi được ghi lại vào bộ nhớ vật lý.

  2. Nếu dữ liệu trong hai thanh ghi CPU khác nhau sẽ được ghi vào cùng một không gian bộ nhớ, chúng ta không thể dự đoán dữ liệu nào sẽ "tồn tại" khi chúng ta viết mã trong C.

    Trong quá trình lắp ráp, nơi chúng tôi mã hóa việc tải và dỡ các thanh ghi CPU theo cách thủ công, chúng tôi sẽ biết dữ liệu nào còn nguyên vẹn. Nhưng C (rất may) trừu tượng hóa chi tiết này đi.

Vì hai con trỏ có thể trỏ đến cùng một vị trí trong bộ nhớ, điều này có thể dẫn đến mã phức tạp xử lý các va chạm có thể xảy ra .

Mã bổ sung này chậm và làm tổn thương hiệu năng vì nó thực hiện các hoạt động đọc / ghi bộ nhớ thêm, cả hai đều chậm hơn và (có thể) không cần thiết.

Các quy tắc nghiêm ngặt răng cưa cho phép chúng tôi để tránh mã máy dự phòng trong những trường hợp trong đó nó nên được an toàn để giả định rằng hai con trỏ không trỏ đến cùng một khối nhớ (xem thêm các restricttừ khoá).

Các răng cưa nghiêm ngặt cho rằng an toàn khi giả định rằng các con trỏ tới các loại khác nhau trỏ đến các vị trí khác nhau trong bộ nhớ.

Nếu một trình biên dịch thông báo rằng hai con trỏ trỏ đến các loại khác nhau (ví dụ: int *a và a float *), nó sẽ giả sử địa chỉ bộ nhớ là khác nhau và nó sẽ không bảo vệ chống va chạm địa chỉ bộ nhớ, dẫn đến mã máy nhanh hơn.

Ví dụ :

Cho phép đảm nhận chức năng sau:

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

Để xử lý trường hợp a == b(cả hai con trỏ trỏ đến cùng một bộ nhớ), chúng ta cần đặt hàng và kiểm tra cách chúng ta tải dữ liệu từ bộ nhớ vào các thanh ghi CPU, vì vậy mã có thể kết thúc như thế này:

  1. tải abtừ bộ nhớ.

  2. thêm avào b.

  3. lưu btải lại a .

    (lưu từ thanh ghi CPU vào bộ nhớ và tải từ bộ nhớ vào thanh ghi CPU).

  4. thêm bvào a.

  5. lưu a(từ thanh ghi CPU) vào bộ nhớ.

Bước 3 rất chậm vì cần truy cập vào bộ nhớ vật lý. Tuy nhiên, cần phải bảo vệ chống lại các trường hợp ở đó abtrỏ đến cùng một địa chỉ bộ nhớ.

Bí danh nghiêm ngặt sẽ cho phép chúng tôi ngăn chặn điều này bằng cách thông báo cho trình biên dịch rằng các địa chỉ bộ nhớ này khác nhau rõ ràng (trong trường hợp này, sẽ cho phép tối ưu hóa hơn nữa không thể được thực hiện nếu con trỏ chia sẻ địa chỉ bộ nhớ).

  1. Điều này có thể được nói với trình biên dịch theo hai cách, bằng cách sử dụng các loại khác nhau để trỏ đến. I E:

    void merge_two_numbers(int *a, long *b) {...}
  2. Sử dụng restricttừ khóa. I E:

    void merge_two_ints(int * restrict a, int * restrict b) {...}

Bây giờ, bằng cách thỏa mãn quy tắc Stias Aliasing, bước 3 có thể tránh được và mã sẽ chạy nhanh hơn đáng kể.

Trong thực tế, bằng cách thêm restricttừ khóa, toàn bộ chức năng có thể được tối ưu hóa thành:

  1. tải abtừ bộ nhớ.

  2. thêm avào b.

  3. lưu kết quả cả đến ab.

Việc tối ưu hóa này không thể được thực hiện trước đây, vì có thể xảy ra va chạm (nơi absẽ tăng gấp ba thay vì tăng gấp đôi).


với từ khóa hạn chế, ở bước 3, không nên lưu kết quả thành 'b'? Nghe có vẻ như kết quả của tổng kết cũng sẽ được lưu trữ trong 'a'. Có phải 'b' cần được tải lại lần nữa không?
NeilB

1
@NeilB - Yap bạn đúng. Chúng tôi chỉ tiết kiệm b(không tải lại) và tải lại a. Tôi hy vọng nó rõ ràng hơn bây giờ.
Bí ẩn

Bí danh dựa trên loại có thể đã cung cấp một số lợi ích trước đó restrict, nhưng tôi nghĩ rằng trong hầu hết các trường hợp sẽ có hiệu quả hơn và nới lỏng một số ràng buộc registersẽ cho phép nó điền vào một số trường hợp restrictkhông giúp được gì. Tôi không chắc chắn việc coi Tiêu chuẩn là mô tả đầy đủ tất cả các trường hợp mà các lập trình viên nên mong đợi trình biên dịch nhận ra bằng chứng về răng cưa, thay vì chỉ mô tả những nơi mà trình biên dịch phải giả định ngay cả khi không có bằng chứng cụ thể nào về nó .
supercat

Lưu ý rằng mặc dù tải từ RAM chính rất chậm (và có thể trì hoãn lõi CPU trong một thời gian dài nếu các thao tác sau phụ thuộc vào kết quả), tải từ bộ đệm L1 khá nhanh và do đó, việc ghi vào dòng bộ đệm gần đây đã ghi đến cùng lõi. Vì vậy, tất cả ngoại trừ việc đọc hoặc ghi đầu tiên vào một địa chỉ thường sẽ nhanh chóng hợp lý: sự khác biệt giữa truy cập reg / mem addr nhỏ hơn sự khác biệt giữa addr mem / cache.
tò mò

@cantlyguy - mặc dù bạn đúng, "nhanh" trong trường hợp này là tương đối. Bộ đệm L1 có lẽ vẫn chậm hơn so với các thanh ghi CPU (tôi nghĩ chậm hơn 10 lần). Ngoài ra, restricttừ khóa giảm thiểu không chỉ tốc độ của các hoạt động mà cả số lượng của chúng, điều đó có thể có ý nghĩa ... Ý tôi là, hoạt động nhanh nhất hoàn toàn không phải là hoạt động :)
Myst

6

Bí danh nghiêm ngặt không cho phép các loại con trỏ khác nhau vào cùng một dữ liệu.

Bài viết này sẽ giúp bạn hiểu vấn đề một cách chi tiết.


4
Bạn có thể bí danh giữa các tham chiếu và giữa một tham chiếu và một con trỏ là tốt. Xem hướng dẫn của tôi dbp-consulting.com/tutorials/StrictAliasing.html
phorgan1

4
Nó được phép có các loại con trỏ khác nhau cho cùng một dữ liệu. Trường hợp răng cưa nghiêm ngặt xuất hiện là khi cùng một vị trí bộ nhớ được ghi thông qua một loại con trỏ và đọc qua một loại khác. Ngoài ra, một số loại khác nhau được cho phép (ví dụ intvà một cấu trúc có chứa một int).
MM

-3

Về mặt kỹ thuật trong C ++, quy tắc răng cưa nghiêm ngặt có lẽ không bao giờ được áp dụng.

Lưu ý định nghĩa của toán tử ( * toán tử ):

Toán tử unary * thực hiện cảm ứng: biểu thức được áp dụng sẽ là một con trỏ tới một loại đối tượng hoặc một con trỏ tới một loại hàm và kết quả là một giá trị tham chiếu đến đối tượng hoặc hàm mà biểu thức chỉ ra .

Cũng từ định nghĩa của glvalue

Glvalue là một biểu thức có đánh giá xác định danh tính của một đối tượng, (... snip)

Vì vậy, trong bất kỳ dấu vết chương trình được xác định rõ, một giá trị đề cập đến một đối tượng. Vì vậy, cái gọi là quy tắc răng cưa nghiêm ngặt không bao giờ được áp dụng. Đây có thể không phải là những gì các nhà thiết kế muốn.


4
Tiêu chuẩn C sử dụng thuật ngữ "đối tượng" để chỉ một số khái niệm khác nhau. Trong số đó, một chuỗi các byte được phân bổ riêng cho một mục đích nào đó, một tham chiếu không nhất thiết phải độc quyền cho một chuỗi các byte đến / từ đó một giá trị của một loại cụ thể có thể được viết hoặc đọc, hoặc một tham chiếu thực sự có đã hoặc sẽ được truy cập trong một số bối cảnh. Tôi không nghĩ có bất kỳ cách hợp lý nào để định nghĩa thuật ngữ "Đối tượng" phù hợp với tất cả cách mà Tiêu chuẩn sử dụng.
supercat

@supercat Không chính xác. Mặc dù trí tưởng tượng của bạn, nó thực sự khá nhất quán. Trong ISO C, nó được định nghĩa là "vùng lưu trữ dữ liệu trong môi trường thực thi, nội dung có thể biểu thị các giá trị". Trong ISO C ++ có một định nghĩa tương tự. Nhận xét của bạn thậm chí không liên quan hơn câu trả lời vì tất cả những gì bạn đề cập là cách thể hiện để tham chiếu nội dung của các đối tượng , trong khi câu trả lời minh họa khái niệm C ++ (glvalue) về một loại biểu thức liên quan chặt chẽ đến nhận dạng của các đối tượng. Và tất cả các quy tắc răng cưa về cơ bản có liên quan đến danh tính nhưng không phải là nội dung.
FrankHB

1
@FrankHB: Nếu một người tuyên bố int foo;, những gì được truy cập bởi biểu thức lvalue *(char*)&foo? Đó có phải là một đối tượng của loại char? Liệu đối tượng đó có tồn tại cùng lúc với foo? Sẽ viết để foothay đổi giá trị được lưu trữ của loại đối tượng nói trên char? Nếu vậy, có quy tắc nào cho phép giá trị được lưu trữ của một đối tượng loại charđược truy cập bằng cách sử dụng một giá trị của loại intkhông?
supercat

@FrankHB: Khi không có 6.5p7, người ta có thể nói đơn giản rằng mọi vùng lưu trữ đồng thời chứa tất cả các đối tượng thuộc mọi loại có thể phù hợp với vùng lưu trữ đó và việc truy cập vào vùng lưu trữ đó đồng thời truy cập tất cả chúng. Tuy nhiên, việc diễn giải theo cách sử dụng thuật ngữ "đối tượng" trong 6.5p7 sẽ cấm làm bất cứ điều gì với các giá trị không thuộc loại ký tự, rõ ràng sẽ là một kết quả vô lý và hoàn toàn đánh bại mục đích của quy tắc. Hơn nữa, khái niệm "đối tượng" được sử dụng ở mọi nơi khác ngoài 6.5p6 có kiểu thời gian biên dịch tĩnh, nhưng ...
supercat

1
sizeof (int) là 4, khai báo có int i;tạo ra bốn đối tượng của mỗi loại ký tự in addition to one of type int ? I see no way to apply a consistent definition of "object" which would allow for operations on both * (char *) & i` và i. Cuối cùng, không có gì trong Tiêu chuẩn cho phép ngay cả một volatilecon trỏ đủ tiêu chuẩn truy cập vào các thanh ghi phần cứng không đáp ứng định nghĩa về "đối tượng".
supercat
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.