Tại sao tràn số học bị bỏ qua?


76

Bạn đã bao giờ thử tổng hợp tất cả các số từ 1 đến 2.000.000 bằng ngôn ngữ lập trình yêu thích của mình chưa? Kết quả rất dễ dàng để tính toán thủ công: 2.000.001.000.000, lớn hơn 900 lần so với giá trị tối đa của số nguyên 32 bit không dấu.

C # in ra -1453759936- một giá trị âm! Và tôi đoán Java cũng làm như vậy.

Điều đó có nghĩa là có một số ngôn ngữ lập trình phổ biến bỏ qua Số học tràn theo mặc định (trong C #, có các tùy chọn ẩn để thay đổi điều đó). Đó là một hành vi có vẻ rất nguy hiểm đối với tôi, và đó không phải là sự cố của Ariane 5 do sự cố tràn như vậy sao?

Vì vậy: các quyết định thiết kế đằng sau một hành vi nguy hiểm như vậy là gì?

Biên tập:

Các câu trả lời đầu tiên cho câu hỏi này thể hiện chi phí quá cao của việc kiểm tra. Hãy thực hiện một chương trình C # ngắn để kiểm tra giả định này:

Stopwatch watch = Stopwatch.StartNew();
checked
{
    for (int i = 0; i < 200000; i++)
    {
        int sum = 0;
        for (int j = 1; j < 50000; j++)
        {
            sum += j;
        }
    }
}
watch.Stop();
Console.WriteLine(watch.Elapsed.TotalMilliseconds);

Trên máy của tôi, phiên bản đã kiểm tra mất 11015ms, trong khi phiên bản không được kiểm tra mất 4125ms. Tức là các bước kiểm tra mất gần gấp đôi thời gian thêm số (tổng cộng gấp 3 lần thời gian ban đầu). Nhưng với 10.000.000.000 lần lặp lại, thời gian thực hiện bằng séc vẫn chưa đến 1 nano giây. Có thể có tình huống đó là quan trọng, nhưng đối với hầu hết các ứng dụng, điều đó không thành vấn đề.

Chỉnh sửa 2:

Tôi đã biên dịch lại ứng dụng máy chủ của chúng tôi (một dịch vụ phân tích dữ liệu Windows nhận được từ một số cảm biến, khá nhiều số bị hỏng) với /p:CheckForOverflowUnderflow="false"tham số (thông thường, tôi bật kiểm tra tràn) và triển khai trên thiết bị. Giám sát Nagios cho thấy tải CPU trung bình ở mức 17%.

Điều này có nghĩa là lần truy cập hiệu năng được tìm thấy trong ví dụ đã tạo ở trên là hoàn toàn không liên quan đến ứng dụng của chúng tôi.


19
giống như một ghi chú, đối với C #, bạn có thể sử dụng checked { }phần để đánh dấu các phần của mã sẽ thực hiện kiểm tra Số học tràn. Điều này là do hiệu suất
Paweł ukasik

14
"Bạn đã bao giờ thử tổng hợp tất cả các số từ 1 đến 2.000.000 bằng ngôn ngữ lập trình yêu thích của mình chưa?" - Vâng : (1..2_000_000).sum #=> 2000001000000. Một trong những ngôn ngữ yêu thích của tôi : sum [1 .. 2000000] --=> 2000001000000. Không yêu thích của tôi : Array.from({length: 2000001}, (v, k) => k).reduce((acc, el) => acc + el) //=> 2000001000000. (Công bằng mà nói, người cuối cùng là gian lận.)
Jörg W Mittag

27
@BernhardHiller Integertrong Haskell là chính xác tùy ý, nó sẽ giữ bất kỳ số nào miễn là bạn không dùng hết RAM phân bổ.
Polygnome

50
Vụ tai nạn Ariane 5 xảy ra do kiểm tra sự cố tràn không thành vấn đề - tên lửa nằm trong một phần của chuyến bay nơi kết quả tính toán thậm chí không cần thiết nữa. Thay vào đó, tràn đã được phát hiện, và điều đó khiến chuyến bay phải hủy bỏ.
Simon B

9
But with the 10,000,000,000 repetitions, the time taken by a check is still less than 1 nanosecond.đó là một dấu hiệu của vòng lặp được tối ưu hóa. Ngoài ra, câu đó mâu thuẫn với những con số trước đó có vẻ rất hợp lệ đối với tôi.
usr

Câu trả lời:


86

Có 3 lý do cho việc này:

  1. Chi phí kiểm tra tràn (đối với mọi hoạt động số học đơn lẻ) tại thời điểm chạy là quá mức.

  2. Sự phức tạp của việc chứng minh rằng kiểm tra tràn có thể được bỏ qua tại thời gian biên dịch là quá mức.

  3. Trong một số trường hợp (ví dụ tính toán CRC, thư viện số lượng lớn, v.v.) "bọc trên tràn" thuận tiện hơn cho các lập trình viên.


10
@DmitryGrigoryev unsigned intkhông nên nghĩ đến vì một ngôn ngữ có kiểm tra tràn nên được kiểm tra tất cả các loại số nguyên theo mặc định. Bạn nên viết wrapping unsigned int.
dùng253751

32
Tôi không mua đối số chi phí. CPU kiểm tra tràn trên MỌI phép tính số nguyên và đặt cờ mang trong ALU. Đó là hỗ trợ ngôn ngữ lập trình mà thiếu. Một didOverflow()hàm nội tuyến đơn giản hoặc thậm chí là một biến toàn cục __carrycho phép truy cập vào cờ mang sẽ không tốn thời gian CPU nếu bạn không sử dụng nó.
slebetman

37
@slebetman: Đó là x86. ARM thì không. Ví dụ: ADDkhông đặt carry (bạn cần ADDS). Itanium thậm chí không cờ mang theo. Và ngay cả trên x86, AVX không có cờ mang.
MSalters

30
@slebetman Nó đặt cờ mang, có (trên x86, nhớ bạn). Nhưng sau đó bạn phải đọc cờ mang và quyết định kết quả - đó là phần đắt đỏ. Vì các phép toán số học thường được sử dụng trong các vòng lặp (và các vòng lặp chặt chẽ ở đó), điều này có thể dễ dàng ngăn chặn nhiều tối ưu hóa trình biên dịch an toàn có thể ảnh hưởng rất lớn đến hiệu suất ngay cả khi bạn chỉ cần một lệnh bổ sung (và bạn cần nhiều hơn thế ). Có nghĩa là nó phải là mặc định? Có lẽ, đặc biệt là trong một ngôn ngữ như C #, nơi nói uncheckedlà đủ dễ dàng; nhưng bạn có thể đánh giá quá cao mức độ thường xuyên xảy ra vấn đề.
Luaan

12
ARM addscó giá tương đương add(chỉ là cờ 1 bit hướng dẫn chọn xem cờ mang có được cập nhật hay không). addBẫy hướng dẫn của MIPS khi bị tràn - bạn phải yêu cầu không bẫy quá mức bằng cách sử dụng adduthay thế!
dùng253751

65

Ai nói đó là một sự đánh đổi tồi tệ?!

Tôi chạy tất cả các ứng dụng sản xuất của mình với tính năng kiểm tra tràn. Đây là một tùy chọn trình biên dịch C #. Tôi thực sự đã điểm chuẩn điều này và tôi không thể xác định sự khác biệt. Chi phí truy cập cơ sở dữ liệu để tạo HTML (không phải đồ chơi) làm lu mờ chi phí kiểm tra tràn.

Tôi đánh giá cao thực tế rằng tôi biết rằng không có hoạt động tràn vào sản xuất. Hầu như tất cả các mã sẽ hành xử thất thường khi có tràn. Các lỗi sẽ không lành tính. Dữ liệu tham nhũng có khả năng, vấn đề bảo mật một khả năng.

Trong trường hợp tôi cần hiệu suất, đôi khi là trường hợp này, tôi vô hiệu hóa kiểm tra tràn bằng cách sử dụng unchecked {}trên cơ sở chi tiết. Khi tôi muốn gọi rằng tôi dựa vào một hoạt động không tràn, tôi có thể thêm checked {}vào mã để ghi lại thực tế đó. Tôi rất bận tâm về việc tràn nhưng tôi không nhất thiết phải nhờ vào việc kiểm tra.

Tôi tin rằng nhóm C # đã chọn sai khi họ chọn không kiểm tra tràn theo mặc định nhưng lựa chọn đó hiện bị niêm phong do những lo ngại về khả năng tương thích mạnh. Lưu ý rằng lựa chọn này được thực hiện vào khoảng năm 2000. Phần cứng ít có khả năng hơn và .NET chưa có nhiều lực kéo. Có lẽ .NET muốn thu hút các lập trình viên Java và C / C ++ theo cách này. .NET cũng có nghĩa là có thể gần với kim loại. Đó là lý do tại sao nó có mã không an toàn, cấu trúc và khả năng gọi gốc tuyệt vời mà tất cả những gì Java không có.

Phần cứng của chúng tôi càng nhanh hơn và các trình biên dịch thông minh hơn sẽ có được kiểm tra tràn hấp dẫn hơn theo mặc định.

Tôi cũng tin rằng kiểm tra tràn thường tốt hơn số có kích thước vô hạn. Các số có kích thước vô hạn có chi phí hiệu năng thậm chí còn cao hơn, khó tối ưu hóa hơn (tôi tin) và chúng mở ra khả năng tiêu thụ tài nguyên không giới hạn.

Cách xử lý tràn của JavaScript thậm chí còn tệ hơn. Số JavaScript là dấu phẩy động tăng gấp đôi. Một "tràn" biểu hiện như là để lại bộ số nguyên hoàn toàn chính xác. Kết quả hơi sai sẽ xảy ra (chẳng hạn như bị tắt bởi một - điều này có thể biến các vòng lặp hữu hạn thành vô hạn).

Đối với một số ngôn ngữ như kiểm tra tràn C / C ++ theo mặc định rõ ràng là không phù hợp vì các loại ứng dụng đang được viết bằng các ngôn ngữ này cần hiệu năng kim loại trần. Tuy nhiên, vẫn có những nỗ lực để biến C / C ++ thành ngôn ngữ an toàn hơn bằng cách cho phép chọn tham gia vào chế độ an toàn hơn. Điều này rất đáng khen ngợi vì 90-99% mã có xu hướng lạnh. Một ví dụ là fwrapvtùy chọn trình biên dịch buộc gói bổ sung của 2. Đây là một tính năng "chất lượng thực hiện" của trình biên dịch, không phải bởi ngôn ngữ.

Haskell không có ngăn xếp cuộc gọi logic và không có thứ tự đánh giá được chỉ định. Điều này làm cho ngoại lệ xảy ra ở những điểm không thể đoán trước. Trong a + bđó không xác định được ahay không bđược đánh giá đầu tiên và liệu các biểu thức đó có chấm dứt hay không. Do đó, thật hợp lý khi Haskell sử dụng các số nguyên không giới hạn hầu hết thời gian. Lựa chọn này phù hợp với ngôn ngữ chức năng thuần túy vì các trường hợp ngoại lệ thực sự không phù hợp trong hầu hết mã Haskell. Và chia cho số 0 thực sự là một điểm có vấn đề trong thiết kế ngôn ngữ Haskells. Thay vì các số nguyên không giới hạn, họ cũng có thể đã sử dụng các số nguyên gói có độ rộng cố định nhưng điều đó không phù hợp với chủ đề "tập trung vào tính chính xác" mà ngôn ngữ có.

Một thay thế cho ngoại lệ tràn là các giá trị độc được tạo bởi các hoạt động không xác định và lan truyền thông qua các hoạt động (như NaNgiá trị float ). Điều đó có vẻ tốn kém hơn nhiều so với kiểm tra tràn và làm cho tất cả các hoạt động chậm hơn, không chỉ những hoạt động có thể thất bại (cấm tăng tốc phần cứng mà thường có và ints thường không có - mặc dù Itanium có NaT là "Không phải là một điều" ). Tôi cũng không hoàn toàn thấy điểm khiến chương trình tiếp tục khập khiễng cùng với dữ liệu xấu. Nó giống như ON ERROR RESUME NEXT. Nó che giấu lỗi nhưng không giúp có được kết quả chính xác. Supercat chỉ ra rằng đôi khi nó là một tối ưu hóa hiệu suất để làm điều này.


2
Câu trả lời tuyệt vời. Vậy lý thuyết của bạn về lý do tại sao họ quyết định làm theo cách đó? Chỉ cần sao chép tất cả những người khác đã sao chép C và cuối cùng là lắp ráp và nhị phân?
jpmc26

19
Khi 99% cơ sở người dùng của bạn mong đợi một hành vi, bạn có xu hướng cung cấp cho họ. Và đối với "sao chép C", nó thực sự không phải là bản sao của C, mà là phần mở rộng của nó. C đảm bảo hành vi ngoại lệ miễn phí unsignedchỉ cho số nguyên. Hành vi của tràn số nguyên đã ký thực sự là hành vi không xác định trong C và C ++. Có, hành vi không xác định . Nó chỉ xảy ra đến mức gần như tất cả mọi người thực hiện nó như là phần bù bổ sung của 2. C # thực sự làm cho nó chính thức, thay vì để nó UB như C / C ++
Cort Ammon

10
@CortAmmon: Ngôn ngữ mà Dennis Ritchie thiết kế đã xác định hành vi bao quanh cho các số nguyên đã ký, nhưng không thực sự phù hợp để sử dụng trên các nền tảng bổ sung không có hai. Mặc dù việc cho phép một số sai lệch nhất định so với phép bổ trợ chính xác của hai phần tử có thể hỗ trợ rất nhiều cho việc tối ưu hóa (ví dụ: cho phép trình biên dịch thay thế x * y / y bằng x có thể lưu một phép nhân và phép chia), các tác giả của trình biên dịch đã giải thích Hành vi không xác định không phải là cơ hội để làm điều gì có ý nghĩa đối với một nền tảng mục tiêu và lĩnh vực ứng dụng nhất định, mà là một cơ hội để ném cảm giác ra khỏi cửa sổ.
supercat

3
@CortAmmon - Kiểm tra mã được tạo bởi gcc -O2for x + 1 > x(đâu xlà một int). Đồng thời xem gcc.gnu.org/onlinesocs/gcc-6.3.0/gcc/ . Hành vi bổ sung 2s trên tràn tràn đã ký trong C là tùy chọn , ngay cả trong trình biên dịch thực và gccmặc định bỏ qua nó ở mức tối ưu hóa thông thường.
Jonathan Cast

2
@supercat Vâng, hầu hết các nhà văn trình biên dịch C đều quan tâm đến việc đảm bảo một số điểm chuẩn không thực tế chạy nhanh hơn 0,5% so với việc cố gắng cung cấp ngữ nghĩa hợp lý cho các lập trình viên (vâng tôi hiểu tại sao nó không phải là một vấn đề dễ giải quyết và có một số tối ưu hợp lý có thể gây ra kết quả bất ngờ khi kết hợp, yada, yada nhưng vẫn không có trọng tâm và bạn nhận thấy nó nếu bạn theo dõi các cuộc hội thoại). May mắn thay, có một số người cố gắng làm tốt hơn .
Voo

30

Bởi vì đó là một trade-off xấu để làm cho tất cả các tính toán rất nhiều tốn kém hơn để tự động bắt trường hợp hiếm hoi mà một tràn không xảy ra. Sẽ tốt hơn nhiều khi tạo gánh nặng cho lập trình viên khi nhận ra các trường hợp hiếm gặp trong đó đây là vấn đề và thêm các biện pháp phòng ngừa đặc biệt hơn là khiến tất cả các lập trình viên phải trả giá cho chức năng mà họ không sử dụng.


28
Điều đó bằng cách nào đó giống như nói rằng nên bỏ qua kiểm tra Buffer Overflow vì chúng hầu như không xảy ra ...
Bernhard Hiller

73
@BernhardHiller: và đó chính xác là những gì C và C ++ làm.
Michael Borgwardt

12
@DavidBrown: Cũng như tràn số học. Các cựu không thỏa hiệp VM mặc dù.
Ded repeatator

35
@Ded repeatator làm cho một điểm tuyệt vời. CLR được thiết kế cẩn thận để các chương trình có thể kiểm chứng không thể vi phạm các bất biến của thời gian chạy ngay cả khi nội dung xấu xảy ra. Các chương trình an toàn tất nhiên có thể vi phạm bất biến của chính họ khi những thứ xấu xảy ra.
Eric Lippert

7
@svick Các phép toán số học có lẽ phổ biến hơn nhiều so với các phép toán lập chỉ mục mảng. Và hầu hết các kích thước số nguyên đều đủ lớn để rất hiếm khi thực hiện số học tràn ra. Vì vậy, tỷ lệ chi phí-lợi ích là rất khác nhau.
Barmar

20

các quyết định thiết kế đằng sau một hành vi nguy hiểm như vậy là gì?

"Đừng buộc người dùng phải trả tiền phạt hiệu suất cho một tính năng mà họ có thể không cần."

Đây là một trong những nguyên lý cơ bản nhất trong thiết kế của C và C ++, và bắt nguồn từ một thời điểm khác khi bạn phải trải qua những mâu thuẫn lố bịch để có được hiệu suất vừa đủ cho các nhiệm vụ ngày nay được coi là tầm thường.

Các ngôn ngữ mới hơn phá vỡ thái độ này đối với nhiều tính năng khác, chẳng hạn như kiểm tra giới hạn mảng. Tôi không chắc tại sao họ không làm điều đó để kiểm tra tràn; nó có thể chỉ đơn giản là một sự giám sát.


18
Đây chắc chắn không phải là một sự giám sát trong thiết kế của C #. Các nhà thiết kế của C # đã cố tình tạo hai chế độ: checkedunchecked, thêm cú pháp để chuyển đổi giữa chúng cục bộ và chuyển đổi dòng lệnh (và cài đặt dự án trong VS) để thay đổi nó trên toàn cầu. Bạn có thể không đồng ý với việc tạo uncheckedmặc định (tôi làm), nhưng tất cả điều này rõ ràng là rất có chủ ý.
Svick

8
@slebetman - chỉ dành cho hồ sơ: chi phí ở đây không phải là chi phí kiểm tra tràn (mà là tầm thường), mà là chi phí chạy mã khác nhau tùy thuộc vào việc tràn xảy ra (rất tốn kém). CPU không thích các câu lệnh nhánh có điều kiện.
Jonathan Cast

5
@jcast Sẽ không dự đoán chi nhánh trên bộ xử lý hiện đại gần như loại bỏ hình phạt tuyên bố chi nhánh có điều kiện? Sau tất cả các trường hợp bình thường không được tràn, do đó, hành vi phân nhánh rất dễ đoán.
CodeMonkey

4
Đồng ý với @CodeMonkey. Một trình biên dịch sẽ đưa vào một bước nhảy có điều kiện trong trường hợp tràn, đến một trang thường không được tải / lạnh. Dự đoán mặc định cho điều đó là "không được thực hiện" và có lẽ nó sẽ không thay đổi. Tổng chi phí là một chỉ dẫn trong đường ống. Nhưng đó là một chi phí hướng dẫn cho mỗi hướng dẫn số học.
MSalters

2
@MSalters có, có một hướng dẫn bổ sung. Và tác động có thể lớn nếu bạn gặp vấn đề liên quan đến CPU. Trong hầu hết các ứng dụng có sự kết hợp giữa mã nặng IO và CPU, tôi cho rằng tác động là tối thiểu. Tôi thích cách Rust, chỉ thêm chi phí trong các bản dựng Debug, nhưng loại bỏ nó trong các bản dựng Phát hành.
CodeMonkey

20

Di sản

Tôi muốn nói rằng vấn đề có thể bắt nguồn từ di sản. Trong C:

  • tràn tràn đã ký là hành vi không xác định (trình biên dịch hỗ trợ cờ để làm cho nó bao bọc),
  • tràn không dấu được xác định hành vi (nó kết thúc tốt đẹp).

Điều này đã được thực hiện để có được hiệu suất tốt nhất có thể, theo nguyên tắc mà lập trình viên biết nó đang làm gì .

Dẫn đến Statu-Quo

Việc C (và bởi phần mở rộng C ++) không yêu cầu phát hiện lần lượt tràn có nghĩa là việc kiểm tra tràn là chậm.

Phần cứng chủ yếu phục vụ cho C / C ++ (nghiêm túc, x86 có một strcmphướng dẫn (còn gọi là PCMPISTRI kể từ SSE 4.2)!) Và vì C không quan tâm, các CPU thông thường không cung cấp các cách hiệu quả để phát hiện tràn. Trong x86, bạn phải kiểm tra cờ mỗi lõi sau mỗi hoạt động có khả năng tràn; khi kết quả bạn muốn là một lá cờ "nhuốm màu" trên kết quả (giống như tuyên truyền của NaN). Và các hoạt động vector có thể còn nhiều vấn đề hơn. Một số người chơi mới có thể xuất hiện trên thị trường với khả năng xử lý tràn hiệu quả; nhưng bây giờ x86 và ARM không quan tâm.

Trình tối ưu hóa trình biên dịch không tốt trong việc tối ưu hóa kiểm tra tràn, hoặc thậm chí tối ưu hóa khi có tràn. Một số học giả như John Regher phàn nàn về statu-quo này , nhưng thực tế là khi thực tế đơn giản tạo ra "thất bại" ngăn chặn tối ưu hóa ngay cả trước khi lắp ráp CPU có thể bị tê liệt. Đặc biệt là khi nó ngăn chặn tự động vector hóa ...

Với hiệu ứng xếp tầng

Vì vậy, trong trường hợp không có chiến lược tối ưu hóa hiệu quả và hỗ trợ CPU hiệu quả, việc kiểm tra tràn là tốn kém. Chi phí cao hơn nhiều so với gói.

Thêm vào một số hành vi gây phiền nhiễu, chẳng hạn như x + y - 1có thể tràn khi x - 1 + ykhông, điều này có thể gây khó chịu cho người dùng một cách hợp pháp và kiểm tra tràn thường được loại bỏ theo hướng có lợi cho việc gói (xử lý ví dụ này và nhiều người khác một cách duyên dáng).

Tuy nhiên, không phải tất cả hy vọng đã mất

Đã có một nỗ lực trong trình biên dịch clang và gcc để thực hiện "chất khử trùng": cách để các công cụ nhị phân phát hiện các trường hợp Hành vi không xác định. Khi sử dụng -fsanitize=undefined, tràn ký đã được phát hiện và hủy bỏ chương trình; rất hữu ích trong quá trình thử nghiệm.

Các Rust ngôn ngữ lập trình có tràn kiểm tra kích hoạt theo mặc định trong chế độ Debug (nó sử dụng gói số học trong chế độ Release vì lý do hiệu suất).

Vì vậy, ngày càng có mối lo ngại về việc kiểm tra tràn và sự nguy hiểm của kết quả không có thật sẽ không bị phát hiện, và hy vọng điều này sẽ lần lượt gây hứng thú trong cộng đồng nghiên cứu, cộng đồng biên dịch và cộng đồng phần cứng.


6
@DmitryGrigoryev đó là đối diện của một cách hiệu quả để kiểm tra tràn, ví dụ trên Haswell nó làm giảm thông lượng từ 4 bổ sung bình thường mỗi chu kỳ chỉ 1 kiểm tra bổ sung, và đó là trước khi xem xét tác động của mispredictions chi nhánh của jo's, và ảnh hưởng toàn cầu hơn của ô nhiễm mà họ thêm vào trạng thái dự đoán nhánh và kích thước mã tăng lên. Nếu lá cờ đó bị dính, nó sẽ mang lại một số tiềm năng thực sự .. và sau đó bạn vẫn không thể thực hiện đúng cách trong mã vector.

3
Vì bạn đang liên kết đến một bài đăng trên blog được viết bởi John Regehr, tôi nghĩ rằng nó cũng phù hợp để liên kết đến một bài viết khác của anh ấy , được viết vài tháng trước khi bạn liên kết. Các bài viết này nói về các triết lý khác nhau: Trong bài viết trước, số nguyên có kích thước cố định; số học số nguyên được kiểm tra (tức là mã không thể tiếp tục thực hiện); có một ngoại lệ hoặc một cái bẫy. Bài báo mới hơn nói về việc bỏ hoàn toàn các số nguyên có kích thước cố định, giúp loại bỏ tràn.
rwong

2
@rwong Số nguyên có kích thước vô hạn cũng có vấn đề của họ. Nếu lỗi tràn của bạn là kết quả của một lỗi (thường xảy ra), nó có thể biến một sự cố nhanh chóng thành một nỗi đau kéo dài tiêu tốn tất cả tài nguyên máy chủ cho đến khi mọi thứ thất bại khủng khiếp. Tôi hầu hết là một người hâm mộ phương pháp "thất bại sớm" - ít có khả năng đầu độc toàn bộ môi trường. 1..100Thay vào đó, tôi thích các loại Pascal-ish hơn - rõ ràng về các phạm vi dự kiến, thay vì bị "ép buộc" thành 2 ^ 31, v.v ... Một số ngôn ngữ cung cấp điều này, tất nhiên, và chúng có xu hướng kiểm tra tràn theo mặc định (đôi khi tại biên dịch thời gian, thậm chí).
Luaan

1
@Luaan: Điều thú vị là thường các lần tính toán trung gian có thể tạm thời bị tràn, nhưng kết quả thì không. Ví dụ: trên phạm vi 1..100 của bạn, x * 2 - 2có thể tràn khi x51, mặc dù kết quả phù hợp, buộc bạn phải sắp xếp lại tính toán của mình (đôi khi theo cách không tự nhiên). Theo kinh nghiệm của tôi, tôi thấy rằng tôi thường thích chạy tính toán theo loại lớn hơn và sau đó kiểm tra xem kết quả có phù hợp hay không.
Matthieu M.

1
@MatthieuM. Vâng, đó là nơi bạn có được vào lãnh thổ "trình biên dịch đủ thông minh". Lý tưởng nhất là giá trị 103 phải hợp lệ cho loại 1..100 miễn là nó không bao giờ được sử dụng trong bối cảnh dự kiến ​​1..100 đúng (ví dụ: x = x * 2 - 2sẽ hoạt động cho tất cả các xtrường hợp chuyển nhượng trong 1 hợp lệ. .100 số). Nghĩa là, các hoạt động trên loại số có thể có độ chính xác cao hơn chính loại đó miễn là việc gán phù hợp. Điều này sẽ khá hữu ích trong các trường hợp như (a + b) / 2bỏ qua (không dấu) tràn có thể là lựa chọn chính xác.
Luaan

10

Các ngôn ngữ cố gắng phát hiện tràn đã được lịch sử xác định ngữ nghĩa liên quan theo cách hạn chế nghiêm trọng những gì có thể là tối ưu hóa hữu ích. Trong số những thứ khác, mặc dù sẽ rất hữu ích khi thực hiện các tính toán theo trình tự khác với những gì được chỉ định trong mã, hầu hết các ngôn ngữ bẫy đều đảm bảo mã được cung cấp như:

for (int i=0; i<100; i++)
{
  Operation1();
  x+=i;
  Operation2();
}

nếu giá trị bắt đầu của x sẽ gây ra tràn tràn trên đường chuyền thứ 47 qua vòng lặp, thì Operation1 sẽ thực thi 47 lần và Operation2 sẽ thực thi 46. Trong trường hợp không có bảo đảm như vậy, nếu không có gì khác trong vòng lặp sử dụng x và không có gì sẽ sử dụng giá trị của x sau một ngoại lệ được ném bởi Operation1 hoặc Operation2, mã có thể được thay thế bằng:

x+=4950;
for (int i=0; i<100; i++)
{
  Operation1();
  Operation2();
}

Thật không may, thực hiện tối ưu hóa như vậy trong khi đảm bảo ngữ nghĩa chính xác trong trường hợp xảy ra tràn trong vòng lặp - về cơ bản đòi hỏi một cái gì đó như:

if (x < INT_MAX-4950)
{
  x+=4950;
  for (int i=0; i<100; i++)
  {
    Operation1();
    Operation2();
  }
}
else
{
  for (int i=0; i<100; i++)
  {
    Operation1();
    x+=i;
    Operation2();
  }
}

Nếu người ta cho rằng rất nhiều mã trong thế giới thực sử dụng các vòng lặp có liên quan nhiều hơn, thì rõ ràng việc tối ưu hóa mã trong khi bảo tồn ngữ nghĩa tràn là khó khăn. Hơn nữa, do các vấn đề về bộ nhớ đệm, việc tăng kích thước mã sẽ khiến chương trình tổng thể chạy chậm hơn mặc dù có ít thao tác hơn trên đường dẫn thường được thực hiện.

Những gì cần thiết để làm cho phát hiện tràn không tốn kém sẽ là một tập hợp các ngữ nghĩa phát hiện tràn tràn được xác định để giúp mã dễ dàng báo cáo liệu một phép tính được thực hiện mà không có bất kỳ tràn nào có thể ảnh hưởng đến kết quả (*), nhưng không gây gánh nặng trình biên dịch với các chi tiết ngoài đó. Nếu một thông số ngôn ngữ được tập trung vào việc giảm chi phí phát hiện tràn đến mức tối thiểu cần thiết để đạt được điều trên, thì nó có thể được thực hiện với chi phí thấp hơn nhiều so với ngôn ngữ hiện có. Tuy nhiên, tôi không biết về bất kỳ nỗ lực nào để tạo điều kiện phát hiện tràn hiệu quả.

(*) Nếu một ngôn ngữ hứa rằng tất cả các luồng tràn sẽ được báo cáo, thì một biểu thức như x*y/ykhông thể được đơn giản hóa xtrừ khi x*ycó thể được đảm bảo không tràn. Tương tự như vậy, ngay cả khi kết quả tính toán sẽ bị bỏ qua, một ngôn ngữ hứa sẽ báo cáo tất cả các lỗi tràn sẽ cần thực hiện bằng mọi cách để nó có thể thực hiện kiểm tra tràn. Vì tràn trong các trường hợp như vậy không thể dẫn đến hành vi không chính xác, nên một chương trình sẽ không cần thực hiện các kiểm tra như vậy để đảm bảo rằng không có tràn nào gây ra kết quả không chính xác.

Ngẫu nhiên, tràn vào C là đặc biệt xấu. Mặc dù hầu hết mọi nền tảng phần cứng hỗ trợ C99 đều sử dụng ngữ nghĩa đóng gói im lặng bổ sung của hai, nhưng đó là thời trang cho các trình biên dịch hiện đại để tạo mã có thể gây ra tác dụng phụ tùy ý trong trường hợp tràn. Ví dụ: đưa ra một cái gì đó như:

#include <stdint.h>
uint32_t test(uint16_t x, uint16_t y) { return x*y & 65535u; }
uint32_t test2(uint16_t q, int *p)
{
  uint32_t total=0;
  q|=32768;
  for (int i = 32768; i<=q; i++)
  {
    total+=test(i,65535);
    *p+=1;
  }
  return total;
}

GCC sẽ tạo mã cho test2 tăng vô điều kiện (* p) một lần và trả về 32768 bất kể giá trị được chuyển vào q. Theo lý luận của nó, tính toán của (32769 * 65535) & 65535u sẽ gây ra tràn và do đó không cần trình biên dịch xem xét bất kỳ trường hợp nào trong đó (q | 32768) sẽ mang lại giá trị lớn hơn 32768. Mặc dù không có Vì lý do tính toán của (32769 * 65535) & 65535u nên quan tâm đến các bit trên của kết quả, gcc sẽ sử dụng tính năng tràn đã ký như là biện minh cho việc bỏ qua vòng lặp.


2
"nó là thời trang cho các trình biên dịch hiện đại ..." - tương tự, nó là thời trang ngắn cho các nhà phát triển của một số hạt nhân nổi tiếng nhất định chọn không đọc tài liệu về các cờ tối ưu hóa mà họ đã sử dụng, và sau đó hành động tức giận trên internet bởi vì họ buộc phải thêm nhiều cờ biên dịch hơn nữa để có được hành vi họ muốn ;-). Trong trường hợp này, -fwrapvdẫn đến hành vi được xác định, mặc dù không phải là hành vi mà người hỏi muốn. Cấp, tối ưu hóa gcc không biến bất kỳ loại phát triển C nào thành một bài kiểm tra kỹ lưỡng về tiêu chuẩn và hành vi của trình biên dịch.
Steve Jessop

1
@SteveJessop: C sẽ là một ngôn ngữ lành mạnh hơn nhiều nếu người viết trình biên dịch nhận ra một phương ngữ cấp thấp trong đó "hành vi không xác định" có nghĩa là "làm bất cứ điều gì có ý nghĩa trên nền tảng cơ bản", và sau đó thêm các cách để lập trình viên từ bỏ các đảm bảo không cần thiết theo đó, thay vì cho rằng cụm từ "không di động hoặc có lỗi" trong Tiêu chuẩn chỉ đơn giản có nghĩa là "lỗi". Trong nhiều trường hợp, mã tối ưu có thể nhận được bằng ngôn ngữ có bảo đảm hành vi yếu sẽ tốt hơn nhiều so với mã được bảo đảm mạnh hơn hoặc không có bảo đảm. Ví dụ ...
supercat

1
... Nếu một lập trình viên cần đánh giá x+y > ztheo kiểu sẽ không bao giờ làm gì khác hơn là mang lại 0 hoặc mang lại 1, nhưng kết quả sẽ được chấp nhận như nhau trong trường hợp tràn, trình biên dịch cung cấp đảm bảo thường có thể tạo mã tốt hơn cho biểu thức x+y > zhơn bất kỳ trình biên dịch nào sẽ có thể tạo cho phiên bản được viết một cách phòng thủ của biểu thức. Nói một cách thực tế, phần tối ưu hóa liên quan đến tràn hữu ích nào sẽ bị loại trừ bởi một đảm bảo rằng các phép tính số nguyên khác với phép chia / phần dư sẽ thực hiện mà không có tác dụng phụ?
supercat

Tôi thú nhận rằng tôi không hoàn toàn đi sâu vào chi tiết, nhưng thực tế là mối hận thù của bạn đối với "người viết trình biên dịch" nói chung, và không cụ thể là "ai đó trên gcc sẽ không chấp nhận -fwhatever-makes-sensebản vá của tôi ", gợi ý cho tôi rằng có nhiều hơn với nó hơn là hay thay đổi về phía họ. Các đối số thông thường mà tôi đã nghe là mã nội tuyến (và thậm chí mở rộng macro) được hưởng lợi từ việc suy luận càng nhiều càng tốt về việc sử dụng cụ thể của một cấu trúc mã, vì một trong những điều thường dẫn đến mã được chèn vào xử lý các trường hợp không cần thiết để, rằng các mã xung quanh "chứng minh" không thể.
Steve Jessop

Vì vậy, đối với một ví dụ đơn giản, nếu tôi viết foo(i + INT_MAX + 1), các trình biên dịch-biên dịch viên rất muốn áp dụng tối ưu hóa cho foo()mã được căn chỉnh, điều này dựa trên tính chính xác của đối số là không âm (có thể là thủ thuật divmod sai lầm). Theo các hạn chế bổ sung của bạn, họ chỉ có thể áp dụng các tối ưu hóa có hành vi cho các đầu vào tiêu cực có ý nghĩa đối với nền tảng. Tất nhiên, cá nhân tôi rất vui vì đó là một -ftùy chọn bật -fwrapv, v.v. và có khả năng phải vô hiệu hóa một số tối ưu hóa không có cờ cho. Nhưng nó không giống như tôi có thể bị làm phiền khi tự mình làm tất cả công việc đó.
Steve Jessop

9

Không phải tất cả các ngôn ngữ lập trình bỏ qua tràn số nguyên. Một số ngôn ngữ cung cấp các hoạt động số nguyên an toàn cho tất cả các số (hầu hết các phương ngữ Lisp, Ruby, Smalltalk, ...) và các ngôn ngữ khác thông qua các thư viện - ví dụ, có nhiều lớp BigInt khác nhau cho C ++.

Việc một ngôn ngữ có làm cho số nguyên an toàn khỏi tràn theo mặc định hay không phụ thuộc vào mục đích của nó: các ngôn ngữ hệ thống như C và C ++ cần cung cấp trừu tượng chi phí bằng 0 và "số nguyên lớn" không phải là một. Các ngôn ngữ năng suất, như Ruby, có thể và thực sự cung cấp các số nguyên lớn. Các ngôn ngữ như Java và C # ở đâu đó ở giữa nên IMHO đi với các số nguyên an toàn ngoài hộp, bởi vì chúng không có.


Lưu ý rằng có một sự khác biệt giữa việc phát hiện tràn (và sau đó có tín hiệu, hoảng loạn, ngoại lệ, ...) và chuyển sang chữ số lớn. Cái trước nên làm rẻ hơn nhiều so với cái trước.
Matthieu M.

@MatthieuM. Hoàn toàn - và tôi nhận ra tôi không rõ về điều đó trong câu trả lời của tôi.
Nemanja Trifunovic

7

Như bạn đã chỉ ra, C # sẽ chậm hơn 3 lần nếu có kiểm tra tràn được bật theo mặc định (giả sử ví dụ của bạn là một ứng dụng điển hình cho ngôn ngữ đó). Tôi đồng ý rằng hiệu suất không phải lúc nào cũng là tính năng quan trọng nhất, nhưng ngôn ngữ / trình biên dịch thường được so sánh về hiệu suất của chúng trong các tác vụ thông thường. Điều này một phần là do chất lượng của các tính năng ngôn ngữ là hơi chủ quan, trong khi một bài kiểm tra hiệu suất là khách quan.

Nếu bạn định giới thiệu một ngôn ngữ mới tương tự như C # trong hầu hết các khía cạnh nhưng chậm hơn 3 lần, việc giành thị phần sẽ không dễ dàng, ngay cả khi cuối cùng, hầu hết người dùng cuối của bạn sẽ được hưởng lợi từ việc kiểm tra tràn hơn so với họ từ hiệu suất cao hơn.


10
Đây là trường hợp đặc biệt đối với C #, thời kỳ đầu so với Java và C ++ không dựa trên các số liệu năng suất của nhà phát triển, rất khó đo lường, hoặc trên các số liệu được lưu bằng tiền mặt từ không giao dịch với bảo mật, vi phạm đó là khó để đo lường, nhưng trên điểm chuẩn hiệu suất tầm thường.
Eric Lippert

1
Và có khả năng, hiệu năng của CPU được kiểm tra với một số lần bẻ khóa đơn giản. Do đó, tối ưu hóa để phát hiện tràn có thể mang lại kết quả "xấu" trong các thử nghiệm đó. Bắt22.
Bernhard Hiller

5

Ngoài nhiều câu trả lời biện minh cho việc thiếu kiểm tra tràn dựa trên hiệu suất, có hai loại số học khác nhau để xem xét:

  1. tính toán lập chỉ mục (lập chỉ mục mảng và / hoặc số học con trỏ)

  2. số học khác

Nếu ngôn ngữ sử dụng kích thước số nguyên giống với kích thước con trỏ, thì chương trình được xây dựng tốt sẽ không tràn khi thực hiện các phép tính lập chỉ mục vì nó nhất thiết phải hết bộ nhớ trước khi tính toán lập chỉ mục sẽ gây ra tràn.

Do đó, kiểm tra phân bổ bộ nhớ là đủ khi làm việc với các biểu thức số học và lập chỉ mục con trỏ liên quan đến các cấu trúc dữ liệu được phân bổ. Ví dụ: nếu bạn có không gian địa chỉ 32 bit và sử dụng số nguyên 32 bit và cho phép tối đa 2GB heap được phân bổ (khoảng một nửa không gian địa chỉ), tính toán lập chỉ mục / con trỏ (về cơ bản) sẽ không tràn.

Hơn nữa, bạn có thể ngạc nhiên khi có bao nhiêu phép cộng / phép trừ / phép nhân liên quan đến việc lập chỉ mục mảng hoặc tính toán con trỏ, do đó rơi vào loại đầu tiên. Con trỏ đối tượng, truy cập trường và các thao tác mảng là các hoạt động lập chỉ mục và nhiều chương trình không tính toán số học nhiều hơn các phép tính này! Về cơ bản, đây là lý do chính mà các chương trình hoạt động tốt như chúng không có kiểm tra tràn số nguyên.

Tất cả các tính toán không lập chỉ mục và không con trỏ nên được phân loại thành các tính toán muốn / mong muốn tràn (ví dụ: tính toán băm) và các tính toán không (ví dụ tổng hợp của bạn).

Trong trường hợp sau, lập trình viên sẽ thường sử dụng các loại dữ liệu thay thế, chẳng hạn như doublehoặc một số BigInt. Nhiều tính toán yêu cầu một decimalloại dữ liệu hơn là double, ví dụ như tính toán tài chính. Nếu chúng không và dính với các kiểu số nguyên, thì chúng cần cẩn thận để kiểm tra tràn số nguyên - hoặc nếu không, vâng, chương trình có thể đạt đến một điều kiện lỗi không bị phát hiện khi bạn chỉ ra.

Là lập trình viên, chúng ta cần phải nhạy cảm với các lựa chọn của mình trong các loại dữ liệu số và hậu quả của chúng về khả năng tràn, chưa kể đến độ chính xác. Nói chung (và đặc biệt là khi làm việc với họ ngôn ngữ C với mong muốn sử dụng các loại số nguyên nhanh), chúng ta cần phải nhạy cảm và nhận thức được sự khác biệt giữa các tính toán lập chỉ mục so với các ngôn ngữ khác.


3

Ngôn ngữ Rust cung cấp một sự thỏa hiệp thú vị giữa việc kiểm tra tràn và không, bằng cách thêm các kiểm tra cho bản dựng gỡ lỗi và xóa chúng trong phiên bản phát hành được tối ưu hóa. Điều này cho phép bạn tìm thấy các lỗi trong quá trình thử nghiệm, trong khi vẫn có được hiệu suất đầy đủ trong phiên bản cuối cùng.

Bởi vì hành vi tràn đôi khi là hành vi mong muốn, cũng có những phiên bản của các toán tử không bao giờ kiểm tra tràn.

Bạn có thể đọc thêm về lý do đằng sau sự lựa chọn trong RFC cho sự thay đổi. Ngoài ra còn có rất nhiều thông tin thú vị trong bài đăng trên blog này , bao gồm một danh sách các lỗi mà tính năng này đã giúp bắt.


2
Rust cũng cung cấp các phương thức như checked_mul, kiểm tra xem liệu tràn đã xảy ra và trả về Nonenếu có, Somenếu không. Điều này có thể được sử dụng trong sản xuất cũng như chế độ gỡ lỗi: doc.rust-lang.org/std/primitive.i32.html#examples-15
Akavall

3

Trong Swift, bất kỳ tràn số nguyên nào được phát hiện theo mặc định và ngay lập tức dừng chương trình. Trong trường hợp bạn cần hành vi bao bọc, có các toán tử khác nhau & +, & - và & * đạt được điều đó. Và có các chức năng thực hiện một hoạt động và cho biết liệu có tràn hay không.

Thật thú vị khi xem những người mới bắt đầu thử đánh giá chuỗi Collatz và gặp sự cố mã của họ :-)

Bây giờ các nhà thiết kế của Swift cũng là nhà thiết kế của LLVM và Clang, vì vậy họ biết một hoặc hai về tối ưu hóa và hoàn toàn có khả năng tránh các kiểm tra tràn không cần thiết. Với tất cả các tối ưu hóa được kích hoạt, kiểm tra tràn không thêm nhiều vào kích thước mã và thời gian thực hiện. Và vì hầu hết các lỗi tràn đều dẫn đến kết quả hoàn toàn không chính xác, nên kích thước mã và thời gian thực hiện được sử dụng rất tốt.

Tái bút Trong C, C ++, tràn số học số nguyên có chữ ký Objective-C là hành vi không xác định. Điều đó có nghĩa là bất cứ điều gì trình biên dịch làm trong trường hợp tràn số nguyên đã ký là chính xác, theo định nghĩa. Các cách điển hình để đối phó với tràn số nguyên đã ký là bỏ qua nó, lấy bất kỳ kết quả nào mà CPU mang lại cho bạn, xây dựng các giả định vào trình biên dịch rằng việc tràn đó sẽ không bao giờ xảy ra (và ví dụ kết luận rằng n + 1> n luôn luôn đúng, vì tràn là giả định là không bao giờ xảy ra) và một khả năng hiếm khi được sử dụng là kiểm tra và xử lý sự cố nếu tràn xảy ra, giống như Swift.


1
Đôi khi tôi đã tự hỏi nếu những người đang thúc đẩy sự điên rồ do UB điều khiển trong C đang bí mật cố gắng làm suy yếu nó để ủng hộ một số ngôn ngữ khác. Điều đó sẽ có ý nghĩa.
supercat

Đối xử x+1>xnhư là vô điều kiện sẽ không yêu cầu trình biên dịch đưa ra bất kỳ "giả định" nào về x nếu trình biên dịch được phép đánh giá các biểu thức số nguyên bằng cách sử dụng các loại lớn hơn tùy ý (hoặc hành xử như thể nó đang làm như vậy). Một ví dụ khó hiểu hơn về các "giả định" dựa trên tràn sẽ quyết định rằng uint32_t mul(uint16_t x, uint16_t y) { return x*y & 65535u; }trình biên dịch được cung cấp có thể sử dụng sum += mul(65535, x)để quyết định rằng xkhông thể lớn hơn 32768 [hành vi có thể gây sốc cho những người đã viết C89 Rationale, cho thấy một trong những yếu tố quyết định. ..
supercat

... trong việc unsigned shortthúc đẩy signed intlà thực tế là hai triển khai đóng gói im lặng bổ sung (nghĩa là phần lớn các triển khai C sau đó được sử dụng) sẽ xử lý mã như trên theo cách tương tự cho dù unsigned shortđược quảng bá inthay unsigned. Tiêu chuẩn không yêu cầu triển khai trên phần cứng bổ sung của hai phần mềm im lặng để xử lý mã như trên, nhưng các tác giả của Tiêu chuẩn dường như đã mong đợi rằng dù sao họ cũng sẽ làm như vậy.
supercat

2

Trên thực tế, nguyên nhân thực sự cho việc này hoàn toàn là về mặt kỹ thuật / lịch sử: dấu hiệu bỏ qua của CPU đối với hầu hết các phần. Nhìn chung, chỉ có một lệnh duy nhất để thêm hai số nguyên vào các thanh ghi và CPU không quan tâm một chút cho dù bạn diễn giải hai số nguyên này là đã ký hay chưa ký. Điều tương tự cũng xảy ra đối với phép trừ và thậm chí cho phép nhân. Hoạt động số học duy nhất cần phải nhận biết dấu hiệu là phân chia.

Lý do tại sao điều này hoạt động, là đại diện bổ sung của 2 số nguyên đã ký được sử dụng bởi hầu như tất cả các CPU. Chẳng hạn, trong phần bổ sung 4 bit 2, việc thêm 5 và -3 trông như thế này:

  0101   (5)
  1101   (-3)
(11010)  (carry)
  ----
  0010   (2)

Quan sát cách hành vi bao quanh của việc vứt bỏ bit thực hiện mang lại kết quả đã ký chính xác. Tương tự, CPU thường thực hiện phép trừ x - ynhư x + ~y + 1:

  0101   (5)
  1100   (~3, binary negation!)
(11011)  (carry, we carry in a 1 bit!)
  ----
  0010   (2)

Điều này thực hiện phép trừ như một phần bổ sung trong phần cứng, chỉ điều chỉnh các đầu vào thành đơn vị logic-đối xứng (ALU) theo những cách tầm thường. Điều gì có thể đơn giản hơn?

Vì phép nhân không có gì khác hơn là một chuỗi các phép cộng, nên nó hoạt động theo cách tương tự tốt đẹp. Kết quả của việc sử dụng biểu diễn bổ sung của 2 và bỏ qua việc thực hiện các phép toán số học là mạch đơn giản hóa và các tập lệnh được đơn giản hóa.

Rõ ràng, vì C được thiết kế để hoạt động gần với kim loại, nên nó đã áp dụng chính xác hành vi này giống như hành vi được tiêu chuẩn hóa của số học không dấu, chỉ cho phép số học được ký để mang lại hành vi không xác định. Và lựa chọn đó được chuyển sang các ngôn ngữ khác như Java và rõ ràng là C #.


Tôi đến đây để đưa ra câu trả lời này là tốt.
Ông Lister

Thật không may, một số người dường như coi là vô lý một cách vô lý khi quan niệm rằng mọi người viết mã C cấp thấp trên nền tảng nên có sự táo bạo để hy vọng rằng trình biên dịch C phù hợp với mục đích như vậy sẽ hành xử theo kiểu hạn chế trong trường hợp bị tràn. Cá nhân, tôi nghĩ hợp lý khi trình biên dịch hoạt động như thể các phép tính được thực hiện bằng cách sử dụng độ chính xác được mở rộng tùy ý để thuận tiện cho trình biên dịch (vì vậy, trên hệ thống 32 bit, nếu x==INT_MAX, sau đó x+1có thể tùy ý hoạt động như +2147483648 hoặc -2147483648 tại trình biên dịch tiện lợi), nhưng ...
supercat

một số người dường như nghĩ rằng nếu xyđang uint16_tvà mã trên hệ thống 32 bit tính toán x*y & 65535ukhi ylà 65535, một trình biên dịch sẽ cho rằng mã sẽ không bao giờ đạt được khi xlớn hơn 32768.
supercat

1

Một số câu trả lời đã thảo luận về chi phí kiểm tra và bạn đã chỉnh sửa câu trả lời của mình để tranh luận rằng đây là một lời biện minh hợp lý. Tôi sẽ cố gắng giải quyết những điểm đó.

Trong C và C ++ (làm ví dụ), một trong những nguyên tắc thiết kế ngôn ngữ là không cung cấp chức năng không được yêu cầu. Điều này thường được tóm tắt bởi cụm từ "không trả tiền cho những gì bạn không sử dụng". Nếu lập trình viên muốn kiểm tra tràn thì họ có thể yêu cầu (và trả tiền phạt). Điều này làm cho ngôn ngữ trở nên nguy hiểm hơn khi sử dụng, nhưng bạn chọn làm việc với ngôn ngữ biết điều đó, vì vậy bạn chấp nhận rủi ro. Nếu bạn không muốn rủi ro đó, hoặc nếu bạn đang viết mã trong đó an toàn là hiệu suất tối quan trọng, thì bạn có thể chọn một ngôn ngữ phù hợp hơn trong đó sự đánh đổi hiệu suất / rủi ro là khác nhau.

Nhưng với 10.000.000.000 lần lặp lại, thời gian thực hiện bằng séc vẫn chưa đến 1 nano giây.

Có một vài điều sai với lý do này:

  1. Đây là môi trường cụ thể. Nói chung rất có ý nghĩa khi trích dẫn các số liệu cụ thể như thế này, bởi vì mã được viết cho tất cả các loại môi trường thay đổi theo thứ tự độ lớn về hiệu suất của chúng. 1 nano giây của bạn trên một máy tính để bàn (tôi giả sử) có vẻ nhanh đến mức đáng kinh ngạc đối với một người nào đó đang mã hóa cho một môi trường nhúng và chậm đến mức không thể chịu đựng được khi ai đó mã hóa cho một cụm siêu máy tính.

  2. 1 nano giây có vẻ như không có gì cho một đoạn mã chạy không thường xuyên. Mặt khác, nếu nó nằm trong một vòng lặp bên trong của một số tính toán là chức năng chính của mã, thì mỗi một phần thời gian bạn có thể cạo đi có thể tạo ra sự khác biệt lớn. Nếu bạn đang chạy một mô phỏng trên một cụm thì những phân số đã lưu trong một nano giây trong vòng lặp bên trong của bạn có thể chuyển trực tiếp sang tiền chi cho phần cứng và điện.

  3. Đối với một số thuật toán và bối cảnh, 10.000.000.000 lần lặp có thể không đáng kể. Một lần nữa, nói chung không có ý nghĩa gì khi nói về các kịch bản cụ thể chỉ áp dụng trong các bối cảnh nhất định.

Có thể có tình huống đó là quan trọng, nhưng đối với hầu hết các ứng dụng, điều đó không thành vấn đề.

Có lẽ bạn đúng. Nhưng một lần nữa, đây là vấn đề mục tiêu của một ngôn ngữ cụ thể là gì. Trên thực tế, nhiều ngôn ngữ được thiết kế để đáp ứng nhu cầu của "hầu hết" hoặc thiên về sự an toàn hơn các mối quan tâm khác. Những người khác, như C và C ++, ưu tiên hiệu quả. Trong bối cảnh đó, khiến mọi người phải trả tiền phạt hiệu suất đơn giản vì hầu hết mọi người sẽ không bị làm phiền, đi ngược lại với những gì ngôn ngữ đang cố gắng đạt được.


-1

Có những câu trả lời hay, nhưng tôi nghĩ rằng có một điểm bị bỏ lỡ ở đây: ảnh hưởng của tràn số nguyên không nhất thiết là điều xấu, và sau thực tế, thật khó để biết liệu việc iđi từ đó có MAX_INTphải MIN_INTlà do vấn đề tràn không hoặc nếu nó được cố ý thực hiện bằng cách nhân với -1.

Ví dụ: nếu tôi muốn cộng tất cả các số nguyên có thể đại diện lớn hơn 0 với nhau, tôi chỉ cần sử dụng một for(i=0;i>=0;++i){...}vòng lặp bổ sung - và khi nó tràn ra, nó dừng lại phép cộng, đó là hành vi mục tiêu (ném lỗi có nghĩa là tôi phải phá vỡ một sự bảo vệ tùy ý vì nó can thiệp vào số học tiêu chuẩn). Đó là thực tế xấu để hạn chế mỹ phẩm nguyên thủy, bởi vì:

  • Chúng được sử dụng trong mọi thứ - sự chậm lại trong toán học nguyên thủy là sự chậm lại trong mọi chương trình hoạt động
  • Nếu một lập trình viên cần chúng, họ luôn có thể thêm chúng
  • Nếu bạn có chúng và lập trình viên không cần chúng (nhưng không cần thời gian chạy nhanh hơn), họ không thể xóa chúng dễ dàng để tối ưu hóa
  • Nếu bạn có chúng và lập trình viên cần chúng không ở đó (như trong ví dụ ở trên), lập trình viên vừa thực hiện cú đánh thời gian chạy (có thể có hoặc không có liên quan), và lập trình viên vẫn cần đầu tư thời gian để loại bỏ hoặc làm việc xung quanh 'bảo vệ'.

3
Một lập trình viên thực sự không thể thêm kiểm tra tràn hiệu quả nếu một ngôn ngữ không cung cấp cho nó. Nếu một hàm tính toán một giá trị bị bỏ qua, trình biên dịch có thể tối ưu hóa tính toán. Nếu một hàm tính toán một giá trị mà là tràn kiểm tra nhưng nếu không thì bỏ qua, một trình biên dịch phải thực hiện các tính toán và bẫy nếu nó tràn, ngay cả khi một tràn nếu không sẽ không ảnh hưởng đến đầu ra của chương trình và có thể được bỏ qua một cách an toàn.
supercat

1
Bạn không thể đi từ INT_MAXđến INT_MINnhân với -1.
David Conrad

Giải pháp rõ ràng là cung cấp một cách để lập trình viên tắt các kiểm tra trong một khối mã hoặc đơn vị biên dịch nhất định.
David Conrad

for(i=0;i>=0;++i){...}là phong cách mã tôi cố gắng làm nản lòng trong nhóm của mình: nó dựa vào các hiệu ứng đặc biệt / tác dụng phụ và không thể hiện rõ ràng ý nghĩa của việc đó. Nhưng tôi vẫn đánh giá cao câu trả lời của bạn vì nó cho thấy một mô hình lập trình khác.
Bernhard Hiller

1
@Delioth: Nếu ilà loại 64 bit, ngay cả khi triển khai với hành vi bổ sung hai vòng im lặng nhất quán, chạy một tỷ lần lặp mỗi giây, vòng lặp như vậy chỉ có thể được đảm bảo để tìm intgiá trị lớn nhất nếu được phép chạy Hàng trăm năm. Trên các hệ thống không hứa hẹn hành vi im lặng nhất quán, các hành vi đó sẽ không được đảm bảo cho dù mã được đưa ra trong bao lâu.
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.