Tại sao kết quả của ++i + ++i + i lại khác nhau với mỗi compiler?

Chào mọi người, mình đang đọc đến phần toán tử ++/-- của C, có làm thử vài ví dụ như bên dưới thì có mấy vấn đề không hiểu, mong mọi người hướng dẫn (nếu ai thấy có bài tương tự rồi thì vui lòng comment link để mình qua đọc, mình tìm không thấy :smiley: :slight_smile:

int main(void) {
unsigned char s = 0;
unsigned char i = 1;

s = ++i + ++i + i;
return 0;
}

Mình dịch chương trình trên bằng compiler GCC (gcc -o main main.c):

  • GCC_MinGW chạy trên Window thì cho kết quả = 6
  • GCC chạy trên Linux thì cho kết quả = 8
  • AVR-GCC chạy trên Arduino thì cho kết quả = 8
  • Mình có thử trên một số trang GCC compiler online thì cũng cho kết quả = 8

Mọi người cho mình hỏi:

  • Sự khác biệt giữa GCC_MinGW và các cái còn lại là gì??
  • Có thể giúp mình giải thích chi tiết cách nào để tính ra kết quả là 8.
    Cảm ơn mọi người!

mấy bài toán như này nên bỏ qua đi bạn học mấy cái có ích hơn như con trỏ chẳng hạn mốt làm nhiều bài tập thường phép toàn trả về người ta dùng những phép toàn đơn giản ko à

Cái biểu thức này thuộc dạng không thể nghĩ bàn :smiley:

1 Like

@Nguyen_Phu_Thanh OK bạn. Nhưng về kết quả ra khác nhau giữa GCC_MinGW và GCC trên Linux và AVR_GCC bạn có thể nói thêm về phần này không.Thank bạn!

Dòng này trong C++ gọi là Undefined behavior, hiểu đơn giản là thứ tự tính i không được quy định, cho nên mỗi compiler sẽ tính toán cho kết quả khác nhau. Bạn hỏi google thêm chi tiết.

2 Likes

http://en.cppreference.com/w/c/language/eval_order

phép tính (a) + (b) trong ngôn ngữ lập trình C ko có nghĩa là (a) được tính trước (b). Có thể (b) trước, rồi (a), hoặc tệ hơn nữa là trong (a) gồm (a1, a2) và trong b gồm (b1,b2,b3) thì có thể được tính đan xen là a1, b1, a2, b2, b3.

(a) + (b) được coi là unsequenced, khác với (ví dụ) (a) && (b), (a) bảo đảm được được tính trước (b)

  1. There is a sequence point after evaluation of the first (left) operand and before evaluation of the second (right) operand of the following binary operators: && (logical AND), || (logical OR), and , (comma).

nếu (a) hoặc (b) có tạo ra side effect trên cùng 1 object, ở đây là thay đổi giá trị của i, thì vì (a) + (b) là unsequenced nên nó bị trở thành undefine behavior

thứ tự đánh giá các arguments trong hàm số f(a,b) cũng tương tự: unsequenced, nên nếu a, b đều gây ra side effects cho cùng 1 object hoặc 1 biến tạo side effect, 1 biến sử dụng giá trị của object bị side effect thì cũng là undefine behavior.

ở đây là C nhé, C++ còn rối loạn hơn nhiều vì tùm lum chuẩn C++11 C++17 @_@

2 Likes

em có thể lên https://godbolt.org/ xem nó biên dịch ra mã máy thế nào

int function()
{
    int i = 1;
    return ++i
     + ++i
     + i;
}

x86-64 GCC -O0 nó biên dịch ra

function:
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], 1
        add     DWORD PTR [rbp-4], 1
        add     DWORD PTR [rbp-4], 1
        mov     eax, DWORD PTR [rbp-4]
        lea     edx, [rax+rax]
        mov     eax, DWORD PTR [rbp-4]
        add     eax, edx
        pop     rbp
        ret

  • mov DWORD PTR [rbp-4], 1 nghĩa là gán i = 1
    2 dòng add tiếp theo nghĩa là tăng giá trị i lên 1, 2 lần.
  • lea edx, [rax+rax] chả hiểu nó làm cái gì @_@ nhưng có lẽ là tính phép cộng đầu tiên, rồi gán vào edx
  • mov eax, DWORD PTR [rbp-4] nó lại vác i lên eax
  • rồi sau cùng tính edx + eax

==> tăng i thêm 1 đơn vị 2 lần, i = 3, rồi tính edx = 3 + 3 = 6, rồi lấy edx + (eax = i), vậy giá trị trả về là 9.

vì side effect của ++i++i ko được sequence, nên nó thích tính ở đâu thì tính, ở đây nó phang 2 cái ++ trước khi lấy giá trị của i cuối cùng.


so với Clang:
function:                               # @function
        push    rbp
        mov     rbp, rsp
        mov     dword ptr [rbp - 4], 1
        mov     eax, dword ptr [rbp - 4]
        add     eax, 1
        mov     dword ptr [rbp - 4], eax
        mov     ecx, dword ptr [rbp - 4]
        add     ecx, 1
        mov     dword ptr [rbp - 4], ecx
        add     eax, ecx
        add     eax, dword ptr [rbp - 4]
        pop     rbp
        ret

ở đây Clang có cảnh báo -Wunsequenced, to mồm hơn và cũng tốt bụng hơn GCC

Clang lại đánh giá kiểu khác:

  • gán i = 1
  • vác i lên eax, tăng eax lên 1 đơn vị (eax = 2), vác eax xuống i lại (i = 2)
  • vác i lên ecx, tăng ecx lên 1 đơn vị (ecx = 3), vác ecx xuống i lại (i = 3)
  • cộng ecx vào eax: eax += 3 (eax = 5)
  • cuối cùng là cộng i vào eax: eax += 3 (eax = 8)

kết quả trả về lại là 8. Vì side effects của ++i ko được sequence nên nó thích đánh giá ở đâu thì nó đánh, Clang đánh theo thứ tự trái sang phải, còn ông GCC thì vác lên phía đầu tiên

(++i) thứ nhất gồm 2 operation là (a1 = gán i=i+1, a2 = trả về giá trị của i), a1 đi trước a2
(++i) thứ hai gồm 2 operation là (b1 = gán i=i+1, b2 = trả về giá trị của i), b1 đi trước b2
(i) gồm 1 operation là (c = trả về giá trị của i)

vì chúng nó ko sequence, GCC được phép đánh giá đan xen là a1, b1, a2, b2, c (a1 vẫn đứng trước a2, b1 vẫn đứng trước b2) => 3 + 3 + 3. Clang cũng được phép đánh giá đan xen nhưng anh chơi “đẹp” đánh giá theo thứ tự trái sang phải a1, a2, b1, b2, c => 2 + 3 + 3.

4 Likes

Cảm ơn mọi người!
@tntxtnt mình cũng đọc qua phần dịch assembly trước khi hỏi, cũng thấy nhưng không hiểu sao mỗi cái nó lại dịch khác nhau, nay biết thêm phần un-sequenced mới hiểu sao lại khác :D, thật sự cảm ơn bạn rất nhiều!
Vậy theo mình, trong một biểu thức, giả sử cần sử dụng biến i nhiều lần, có chỗ dùng i, có chỗ cần tăng i lên 1 đơn vị trước khi dùng. thì cách đảm bảo an toàn là chỗ cần dùng i tăng 1 đơn vị nên thay bằng (i + 1) đặt trong ngoặc.
nếu trường hợp cần dùng biến nhiều lần như trên mọi người thường dùng theo kiểu nào vậy??

gcc rất hay sử dụng lea. Lệnh này mục đích chính là tính địa chỉ (cho nhanh) nhưng có thể lợi dụng để thực hiện các phép tính add-shift.

Cú pháp [seg] là slot mem được trỏ bởi seg :slight_smile:lea là load effective address.

Không có chuyện đó. Như bạn đã thấy là mạnh ai nấy ra kết quả nên viết kiểu đó thành ra không thể nghĩ bàn :slight_smile:

2 Likes

dùng theo sequence bạn muốn thôi. Tách nó ra thành nhiều câu lệnh nếu cần. Nếu bạn muốn nó luôn luôn ra 8 thì viết là

int a = ++i; //a = 2
int b = ++i; //b = 3
s = a + b + i; //bảo đảm thứ tự đánh giá ở trên: int a = ... trước, rồi tới int b = ...,
               //rồi sau cùng mới thưc hiện phép cộng ko có nhiều side effects cho biến i

viết gộp lại ngắn thật nhưng thằng trình biên dịch nó cũng thích viết ngắn, nó chế ra asm ngắn hơn mà ko theo ý mình thì chết

1 Like

Viết gộp có khi chết không kịp trối ấy chứ. Đã gọi là undefined thì chuyện gì cũng có thể xảy ra, khác với unspecified được chọn tùy ý cách tính.

1 Like

Không phải cứ ngắn là tốt bạn à. Code ngoài để biên dịch ra thì còn để đọc nữa.

1 Like
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?