Tại sao if (biến1% biến2 == 0) không hiệu quả?


179

Tôi mới sử dụng java và đã chạy một số mã tối qua và điều này thực sự làm phiền tôi. Tôi đang xây dựng một chương trình đơn giản để hiển thị mọi đầu ra X trong một vòng lặp for và tôi nhận thấy hiệu suất giảm MASSIVE, khi tôi sử dụng mô-đun là variable % variablevs variable % 5000hoặc không có gì. Ai đó có thể giải thích cho tôi tại sao điều này là và những gì gây ra nó? Vì vậy, tôi có thể tốt hơn ...

Đây là mã "hiệu quả" (xin lỗi nếu tôi có một chút cú pháp sai Tôi không ở trên máy tính với mã ngay bây giờ)

long startNum = 0;
long stopNum = 1000000000L;

for (long i = startNum; i <= stopNum; i++){
    if (i % 50000 == 0) {
        System.out.println(i);
    }
}

Đây là "mã không hiệu quả"

long startNum = 0;
long stopNum = 1000000000L;
long progressCheck = 50000;

for (long i = startNum; i <= stopNum; i++){
    if (i % progressCheck == 0) {
        System.out.println(i);
    }
}

Hãy nhớ rằng tôi có một biến số ngày để đo lường sự khác biệt và một khi nó đủ dài, cái đầu tiên mất 50ms trong khi cái kia mất 12 giây hoặc tương tự. Bạn có thể phải tăng stopNumhoặc giảm progressChecknếu PC của bạn hiệu quả hơn máy của tôi hoặc không.

Tôi đã tìm câu hỏi này trên web, nhưng tôi không thể tìm thấy câu trả lời, có lẽ tôi chỉ không hỏi đúng.

EDIT: Tôi không mong đợi câu hỏi của tôi sẽ phổ biến đến vậy, tôi đánh giá cao tất cả các câu trả lời. Tôi đã thực hiện một điểm chuẩn trên mỗi nửa thời gian thực hiện và mã không hiệu quả mất nhiều thời gian hơn, 1/4 giây so với 10 giây cho hoặc mất. Cứ cho là họ đang sử dụng println, nhưng cả hai đều làm cùng một số tiền, vì vậy tôi sẽ không tưởng tượng rằng nó sẽ làm lệch nó nhiều, đặc biệt là vì sự khác biệt có thể lặp lại. Về câu trả lời, vì tôi chưa quen với Java, tôi sẽ để phiếu bầu quyết định xem câu trả lời nào là tốt nhất. Tôi sẽ cố gắng chọn một vào thứ Tư.

EDIT2: Tôi sẽ thực hiện một thử nghiệm khác vào tối nay, trong đó thay vì mô-đun, nó chỉ tăng một biến và khi đạt đến tiến trình, nó sẽ thực hiện một thử nghiệm, sau đó đặt lại biến đó thành 0. cho tùy chọn thứ 3.

EDIT3,5:

Tôi đã sử dụng mã này và dưới đây tôi sẽ hiển thị kết quả của mình .. Cảm ơn TẤT CẢ vì sự giúp đỡ tuyệt vời! Tôi cũng đã thử so sánh giá trị ngắn của dài với 0, vì vậy tất cả các kiểm tra mới của tôi xảy ra bao giờ "65536" làm cho nó bằng nhau trong các lần lặp lại.

public class Main {


    public static void main(String[] args) {

        long startNum = 0;
        long stopNum = 1000000000L;
        long progressCheck = 65536;
        final long finalProgressCheck = 50000;
        long date;

        // using a fixed value
        date = System.currentTimeMillis();
        for (long i = startNum; i <= stopNum; i++) {
            if (i % 65536 == 0) {
                System.out.println(i);
            }
        }
        long final1 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();
        //using a variable
        for (long i = startNum; i <= stopNum; i++) {
            if (i % progressCheck == 0) {
                System.out.println(i);
            }
        }
        long final2 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();

        // using a final declared variable
        for (long i = startNum; i <= stopNum; i++) {
            if (i % finalProgressCheck == 0) {
                System.out.println(i);
            }
        }
        long final3 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();
        // using increments to determine progressCheck
        int increment = 0;
        for (long i = startNum; i <= stopNum; i++) {
            if (increment == 65536) {
                System.out.println(i);
                increment = 0;
            }
            increment++;

        }

        //using a short conversion
        long final4 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();
        for (long i = startNum; i <= stopNum; i++) {
            if ((short)i == 0) {
                System.out.println(i);
            }
        }
        long final5 = System.currentTimeMillis() - date;

                System.out.println(
                "\nfixed = " + final1 + " ms " + "\nvariable = " + final2 + " ms " + "\nfinal variable = " + final3 + " ms " + "\nincrement = " + final4 + " ms" + "\nShort Conversion = " + final5 + " ms");
    }
}

Các kết quả:

  • Đã sửa = 874 ms (thường là khoảng 1000ms, nhưng nhanh hơn do nó là công suất 2)
  • biến = 8590 ms
  • biến cuối cùng = 1944 ms (Được ~ 1000ms khi sử dụng 50000)
  • gia tăng = 1904 ms
  • Chuyển đổi ngắn = 679 ms

Không đủ ngạc nhiên, do thiếu sự phân chia, Chuyển đổi ngắn nhanh hơn 23% so với cách "nhanh". Đây là điều thú vị cần lưu ý. Nếu bạn cần hiển thị hoặc so sánh một cái gì đó cứ sau 256 lần (hoặc về đó), bạn có thể làm điều này và sử dụng

if ((byte)integer == 0) {'Perform progress check code here'}

MỘT LƯU Ý QUAN TÂM CUỐI CÙNG, sử dụng mô-đun trên "Biến được khai báo cuối cùng" với 65536 (không phải là một số đẹp) là một nửa tốc độ (chậm hơn) so với giá trị cố định. Trường hợp trước khi nó được điểm chuẩn gần cùng tốc độ.


29
Tôi đã nhận được kết quả tương tự thực sự. Trên máy của tôi, vòng lặp đầu tiên chạy trong khoảng 1,5 giây và vòng thứ hai chạy trong khoảng 9 giây. Nếu tôi thêm finalvào trước progressCheckbiến, cả hai đều chạy cùng tốc độ một lần nữa. Điều đó khiến tôi tin rằng trình biên dịch hoặc JIT quản lý để tối ưu hóa vòng lặp khi nó biết đó progressChecklà hằng số.
marstran


24
Division bởi một hằng số có thể dễ dàng chuyển đổi sang một phép nhân bởi nghịch đảo . Chia theo một biến không thể. Và phân chia 32 bit nhanh hơn phân chia 64 bit trên x86
phuclv

2
@phuclv lưu ý Phân chia 32 bit không phải là vấn đề ở đây, đây là hoạt động còn lại 64 bit trong cả hai trường hợp
user85421

4
@RobertCotterman nếu bạn khai báo biến là cuối cùng, trình biên dịch sẽ tạo mã byte tương tự như sử dụng hằng số (nhật thực / Java 11) ((mặc dù sử dụng thêm một khe nhớ cho biến))
user85421

Câu trả lời:


139

Bạn đang đo sơ khai OSR (thay thế trên ngăn xếp) .

Sơ khai OSR là một phiên bản đặc biệt của phương thức được biên dịch dành riêng cho việc chuyển thực thi từ chế độ được dịch sang mã được biên dịch trong khi phương thức đang chạy.

Sơ khai OSR không được tối ưu hóa như các phương thức thông thường, bởi vì chúng cần bố cục khung tương thích với khung được diễn giải. Tôi đã chỉ ra điều này trong các câu trả lời sau: 1 , 2 , 3 .

Một điều tương tự cũng xảy ra ở đây. Trong khi "mã không hiệu quả" đang chạy một vòng lặp dài, phương thức được biên dịch đặc biệt để thay thế trên ngăn xếp ngay bên trong vòng lặp. Trạng thái được chuyển từ khung được giải thích sang phương thức được biên dịch OSR và trạng thái này bao gồm progressCheckbiến cục bộ. Tại thời điểm này, JIT không thể thay thế biến bằng hằng số và do đó không thể áp dụng các tối ưu hóa nhất định như giảm cường độ .

Cụ thể, điều này có nghĩa là JIT không thay thế phép chia số nguyên bằng phép nhân . (Xem Tại sao sử dụng GCC nhân của một số lạ trong việc thực hiện phân chia số nguyên? Cho lừa asm từ một phía trước-of-thời gian biên dịch, khi giá trị là một hằng số thời gian biên dịch sau khi nội tuyến / không đổi công tác tuyên truyền, nếu những tối ưu hóa được kích hoạt Một số nguyên ngay trong %biểu thức cũng được tối ưu hóa gcc -O0, tương tự như ở đây, nơi nó được tối ưu hóa bởi JITer ngay cả trong sơ khai OSR.)

Tuy nhiên, nếu bạn chạy cùng một phương thức nhiều lần, lần chạy thứ hai và lần chạy tiếp theo sẽ thực thi mã thông thường (không phải OSR), được tối ưu hóa hoàn toàn. Dưới đây là điểm chuẩn để chứng minh lý thuyết ( điểm chuẩn bằng JMH ):

@State(Scope.Benchmark)
public class Div {

    @Benchmark
    public void divConst(Blackhole blackhole) {
        long startNum = 0;
        long stopNum = 100000000L;

        for (long i = startNum; i <= stopNum; i++) {
            if (i % 50000 == 0) {
                blackhole.consume(i);
            }
        }
    }

    @Benchmark
    public void divVar(Blackhole blackhole) {
        long startNum = 0;
        long stopNum = 100000000L;
        long progressCheck = 50000;

        for (long i = startNum; i <= stopNum; i++) {
            if (i % progressCheck == 0) {
                blackhole.consume(i);
            }
        }
    }
}

Và kết quả:

# Benchmark: bench.Div.divConst

# Run progress: 0,00% complete, ETA 00:00:16
# Fork: 1 of 1
# Warmup Iteration   1: 126,967 ms/op
# Warmup Iteration   2: 105,660 ms/op
# Warmup Iteration   3: 106,205 ms/op
Iteration   1: 105,620 ms/op
Iteration   2: 105,789 ms/op
Iteration   3: 105,915 ms/op
Iteration   4: 105,629 ms/op
Iteration   5: 105,632 ms/op


# Benchmark: bench.Div.divVar

# Run progress: 50,00% complete, ETA 00:00:09
# Fork: 1 of 1
# Warmup Iteration   1: 844,708 ms/op          <-- much slower!
# Warmup Iteration   2: 105,893 ms/op          <-- as fast as divConst
# Warmup Iteration   3: 105,601 ms/op
Iteration   1: 105,570 ms/op
Iteration   2: 105,475 ms/op
Iteration   3: 105,702 ms/op
Iteration   4: 105,535 ms/op
Iteration   5: 105,766 ms/op

Lần lặp đầu tiên divVarthực sự chậm hơn nhiều, vì sơ khai OSR được biên dịch không hiệu quả. Nhưng ngay khi phương thức chạy lại từ đầu, phiên bản mới không bị ràng buộc được thực thi, nó tận dụng tất cả các tối ưu hóa trình biên dịch có sẵn.


5
Tôi ngần ngại bỏ phiếu về điều này. Một mặt, nó nghe có vẻ như một cách nói công phu "Bạn đã làm hỏng điểm chuẩn của mình, đọc một cái gì đó về JIT". Mặt khác, tôi tự hỏi tại sao bạn dường như rất chắc chắn rằng OSR là điểm liên quan chính ở đây. Ý tôi là, làm một (vi) điểm chuẩn có liên quan đến System.out.printlnsẽ gần như nhất thiết phải tạo ra kết quả rác, và thực tế là cả hai phiên bản là như nhau nhanh không phải làm bất cứ điều gì với OSR trong đặc biệt , như xa như tôi có thể nói ..
Marco13

2
(Tôi tò mò và muốn hiểu điều này. Tôi hy vọng các bình luận không bị làm phiền, có thể xóa chúng sau, nhưng :) Liên kết 1hơi đáng ngờ - vòng lặp trống cũng có thể được tối ưu hóa hoàn toàn. Cái thứ hai giống với cái kia hơn. Nhưng một lần nữa, không rõ lý do tại sao bạn gán sự khác biệt cho OSR một cách cụ thể . Tôi chỉ muốn nói: Tại một số điểm, phương thức này bị JITed và trở nên nhanh hơn. Theo hiểu biết của tôi, OSR chỉ khiến việc sử dụng mã được tối ưu hóa cuối cùng thành (đại khái) là ~ "được hoãn lại để vượt qua tối ưu hóa tiếp theo". (tiếp tục ...)
Marco13

1
(tiếp theo :) Trừ khi bạn đang phân tích cụ thể các bản ghi hotspot, bạn không thể nói liệu sự khác biệt có phải là do so sánh mã JITed và không JITed hay không, hoặc bằng cách so sánh mã JITed và OSR-stub-code. Và bạn chắc chắn không thể nói rằng chắc chắn khi câu hỏi không chứa mã thực hoặc điểm chuẩn JMH hoàn chỉnh. Vì vậy, lập luận rằng sự khác biệt là do âm thanh OSR gây ra, đối với tôi, cụ thể không phù hợp (và "không chính đáng") so với việc nói rằng đó là do JIT nói chung. (Không xúc phạm - Tôi chỉ đang tự hỏi ...)
Marco13

4
@ Marco13 có một heuristic đơn giản: không có hoạt động của JIT, mỗi %thao tác sẽ có cùng trọng số, vì việc thực hiện tối ưu hóa chỉ có thể, nếu một trình tối ưu hóa thực hiện công việc thực tế. Vì vậy, thực tế là một biến thể vòng lặp nhanh hơn đáng kể so với biến thể khác chứng minh sự hiện diện của trình tối ưu hóa và chứng minh thêm rằng nó không thể tối ưu hóa một trong các vòng lặp ở cùng mức độ với nhau (trong cùng một phương thức!). Vì câu trả lời này chứng minh khả năng tối ưu hóa cả hai vòng lặp ở cùng một mức độ, phải có điều gì đó cản trở việc tối ưu hóa. Và đó là OSR trong 99,9% của tất cả các trường hợp
Holger

4
@ Marco13 Đó là một "phỏng đoán có giáo dục" dựa trên kiến ​​thức về HotSpot Runtime và kinh nghiệm phân tích các vấn đề tương tự trước đây. Một vòng lặp dài như vậy khó có thể được biên dịch theo cách khác với OSR, đặc biệt là trong một điểm chuẩn được làm bằng tay đơn giản. Bây giờ, khi OP đã đăng mã hoàn chỉnh, tôi chỉ có thể xác nhận lý do một lần nữa bằng cách chạy mã với -XX:+PrintCompilation -XX:+TraceNMethodInstalls.
apangin

42

Trong phần tiếp theo nhận xét @phuclv , tôi đã kiểm tra mã được tạo bởi JIT 1 , kết quả như sau:

cho variable % 5000(chia theo hằng số):

mov     rax,29f16b11c6d1e109h
imul    rbx
mov     r10,rbx
sar     r10,3fh
sar     rdx,0dh
sub     rdx,r10
imul    r10,rdx,0c350h    ; <-- imul
mov     r11,rbx
sub     r11,r10
test    r11,r11
jne     1d707ad14a0h

cho variable % variable:

mov     rax,r14
mov     rdx,8000000000000000h
cmp     rax,rdx
jne     22ccce218edh
xor     edx,edx
cmp     rbx,0ffffffffffffffffh
je      22ccce218f2h
cqo
idiv    rax,rbx           ; <-- idiv
test    rdx,rdx
jne     22ccce218c0h

Vì phép chia luôn mất nhiều thời gian hơn phép nhân, đoạn mã cuối cùng ít hiệu suất hơn.

Phiên bản Java:

java version "11" 2018-09-25
Java(TM) SE Runtime Environment 18.9 (build 11+28)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11+28, mixed mode)

1 - Tùy chọn VM được sử dụng: -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,src/java/Main.main


14
Để đưa ra một thứ tự cường độ trên "chậm hơn", với x86_64: imullà 3 chu kỳ, idivnằm trong khoảng từ 30 đến 90 chu kỳ. Vì vậy, phép chia số nguyên chậm hơn từ 10 đến 30 lần so với phép nhân số nguyên.
Matthieu M.

2
Bạn có thể giải thích tất cả những điều đó có nghĩa gì đối với những độc giả quan tâm, nhưng không nói được trình biên dịch?
Nico Haase

7
@NicoHaase Hai dòng nhận xét là những dòng quan trọng duy nhất. Trong phần đầu tiên, mã đang thực hiện phép nhân số nguyên, trong khi phần thứ hai, mã đang thực hiện phép chia số nguyên. Nếu bạn nghĩ về việc nhân và chia bằng tay, khi bạn nhân, bạn thường thực hiện một loạt các phép nhân nhỏ và sau đó một tập hợp bổ sung lớn, nhưng phép chia là một phép chia nhỏ, phép nhân nhỏ, phép trừ và lặp lại. Phân chia chậm vì về cơ bản bạn đang thực hiện một loạt các phép nhân.
MBraedley

4
@MBraedley cảm ơn bạn đã đóng góp, nhưng lời giải thích như vậy nên được thêm vào chính câu trả lời và không bị ẩn trong phần bình luận
Nico Haase

6
@MBraedley: Hơn nữa, nhân lên trong CPU hiện đại là nhanh vì các sản phẩm một phần là độc lập và do đó có thể được tính riêng, trong khi mỗi giai đoạn của một bộ phận phụ thuộc vào các giai đoạn trước.
supercat

26

Như những người khác đã lưu ý, hoạt động mô đun chung đòi hỏi phải thực hiện một bộ phận. Trong một số trường hợp, phép chia có thể được thay thế (bằng trình biên dịch) bằng phép nhân. Nhưng cả hai có thể chậm so với cộng / trừ. Do đó, hiệu suất tốt nhất có thể được mong đợi bởi một cái gì đó dọc theo các dòng này:

long progressCheck = 50000;

long counter = progressCheck;

for (long i = startNum; i <= stopNum; i++){
    if (--counter == 0) {
        System.out.println(i);
        counter = progressCheck;
    }
}

(Như một nỗ lực tối ưu hóa nhỏ, chúng tôi sử dụng bộ đếm giảm trước ở đây vì trên nhiều kiến ​​trúc so với 0ngay sau khi hoạt động số học có chi phí chính xác 0 hướng dẫn / chu kỳ CPU vì các cờ của ALU đã được thiết lập phù hợp bởi hoạt động tiền xử lý. tuy nhiên, trình biên dịch sẽ tự động thực hiện việc tối ưu hóa đó ngay cả khi bạn viết if (counter++ == 50000) { ... counter = 0; }.)

Lưu ý rằng thường thì bạn không thực sự muốn / cần mô-đun, bởi vì bạn biết rằng bộ đếm vòng lặp của bạn ( i) hoặc bất cứ thứ gì chỉ tăng thêm 1 và bạn thực sự không quan tâm đến phần còn lại thực tế mà mô-đun sẽ cung cấp cho bạn, chỉ cần nhìn thấy nếu bộ đếm tăng dần đạt một số giá trị.

Một 'mẹo' khác là sử dụng các giá trị / giới hạn của hai giá trị, ví dụ progressCheck = 1024;. Mô-đun một sức mạnh của hai có thể được tính toán nhanh chóng thông qua bitwise and, tức là if ( (i & (1024-1)) == 0 ) {...}. Điều này cũng khá nhanh và có thể trên một số kiến ​​trúc tốt hơn so với tường minhcounter ở trên.


3
Một trình biên dịch thông minh sẽ đảo ngược các vòng lặp ở đây. Hoặc bạn có thể làm điều đó trong nguồn. Cơ if()thể trở thành một cơ thể vòng ngoài, và những thứ bên ngoài if()trở thành một cơ thể vòng lặp bên trong chạy cho các min(progressCheck, stopNum-i)lần lặp. Vì vậy, khi bắt đầu và mỗi khi counterđạt 0, bạn sẽ long next_stop = i + min(progressCheck, stopNum-i);thiết lập một for(; i< next_stop; i++) {}vòng lặp. Trong trường hợp này, vòng lặp bên trong trống và hy vọng sẽ tối ưu hóa hoàn toàn, bạn có thể thực hiện điều đó trong nguồn và giúp JITer dễ dàng, giảm vòng lặp của bạn xuống i + = 50k.
Peter Cordes

2
Nhưng có, nói chung, một bộ đếm xuống là một kỹ thuật hiệu quả tốt cho các loại công cụ fizzbuzz / tiến trình kiểm tra.
Peter Cordes

Tôi đã thêm vào câu hỏi của mình và đã tăng lên, --countertốc độ cũng nhanh như phiên bản gia tăng của tôi, nhưng ít mã hơn. Ngoài ra, nó thấp hơn 1 so với mức cần thiết, tôi tò mò liệu có nên counter--lấy số chính xác mà bạn muốn không , không phải điều đó quá khác biệt
Robert Cotterman

@PeterCordes Một trình biên dịch thông minh sẽ chỉ in các số, không có vòng lặp nào cả. (Tôi nghĩ rằng một số điểm chuẩn tầm thường hơn một chút đã bắt đầu thất bại theo cách đó có thể 10 năm trước.)
Peter - Tái lập Monica

2
@RobertCotterman Có, --counterbị tắt bởi một. counter--sẽ cung cấp cho bạn chính xác progressChecksố lần lặp (hoặc bạn có thể đặt progressCheck = 50001;khóa học).
JimmyB

4

Tôi cũng ngạc nhiên khi thấy hiệu suất của các mã trên. Đó là tất cả về thời gian của trình biên dịch để thực hiện chương trình theo biến khai báo. Trong ví dụ thứ hai (không hiệu quả):

for (long i = startNum; i <= stopNum; i++) {
    if (i % progressCheck == 0) {
        System.out.println(i)
    }
}

Bạn đang thực hiện thao tác mô đun giữa hai biến. Ở đây, trình biên dịch phải kiểm tra giá trị của stopNumprogressCheckđi đến khối bộ nhớ cụ thể được đặt cho các biến này mỗi lần sau mỗi lần lặp bởi vì nó là một biến và giá trị của nó có thể thay đổi.

Đó là lý do tại sao sau mỗi trình biên dịch lặp đi đến vị trí bộ nhớ để kiểm tra giá trị mới nhất của các biến. Do đó, tại thời điểm biên dịch, trình biên dịch không thể tạo mã byte hiệu quả.

Trong ví dụ mã đầu tiên, bạn đang thực hiện toán tử mô đun giữa một biến và một giá trị số không đổi sẽ không thay đổi trong quá trình thực thi và trình biên dịch không cần kiểm tra giá trị của giá trị số đó từ vị trí bộ nhớ. Đó là lý do tại sao trình biên dịch có thể tạo mã byte hiệu quả. Nếu bạn khai báo progressChecklà một biến finalhoặc một final staticbiến thì tại thời điểm trình biên dịch thời gian chạy / biên dịch thời gian chạy biết rằng đó là biến cuối cùng và giá trị của nó sẽ không thay đổi thì trình biên dịch sẽ thay thế progressCheckbằng 50000mã:

for (long i = startNum; i <= stopNum; i++) {
    if (i % 50000== 0) {
        System.out.println(i)
    }
}

Bây giờ bạn có thể thấy rằng mã này cũng giống như ví dụ mã đầu tiên (hiệu quả). Hiệu suất của mã đầu tiên và như chúng tôi đã đề cập ở trên cả hai mã sẽ hoạt động hiệu quả. Sẽ không có nhiều khác biệt về thời gian thực hiện của một trong hai ví dụ mã.


1
Có một sự khác biệt lớn, mặc dù tôi đã thực hiện thao tác một nghìn tỷ lần, vì vậy hơn 1 nghìn tỷ hoạt động, nó đã tiết kiệm 89% thời gian để thực hiện mã "hiệu quả". phiền bạn nếu bạn chỉ làm điều đó vài ngàn lần, đang nói một sự khác biệt nhỏ như vậy, nó có lẽ không phải là một vấn đề lớn. ý tôi là hơn 1000 thao tác nó sẽ giúp bạn tiết kiệm 1 triệu trong 7 giây.
Robert Cotterman

1
@Bishal Dubey "Sẽ không có nhiều khác biệt về thời gian thực hiện của cả hai mã." Bạn đã đọc câu hỏi?
Cấp Foster

"Đó là lý do tại sao sau mỗi trình biên dịch lặp đi đến vị trí bộ nhớ để kiểm tra giá trị mới nhất của các biến" - Trừ khi biến được khai báo volatile, 'trình biên dịch' sẽ không đọc lại giá trị của nó từ RAM.
JimmyB
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.