Tôi nên sử dụng phép nhân hoặc chia?


118

Đây là một câu hỏi vui vẻ ngớ ngẩn:

Giả sử chúng ta phải thực hiện một thao tác đơn giản trong đó chúng ta cần một nửa giá trị của một biến. Có thường hai cách để làm điều này:

y = x / 2.0;
// or...
y = x * 0.5;

Giả sử chúng ta đang sử dụng các toán tử tiêu chuẩn được cung cấp với ngôn ngữ, cái nào có hiệu suất tốt hơn?

Tôi đoán phép nhân thường tốt hơn nên tôi cố gắng tuân theo điều đó khi tôi viết mã, nhưng tôi muốn xác nhận điều này.

Mặc dù cá nhân tôi quan tâm đến câu trả lời cho Python 2.4-2.5, vui lòng gửi câu trả lời cho các ngôn ngữ khác! Và nếu bạn muốn, hãy thoải mái đăng các cách khác của người hâm mộ (như sử dụng toán tử dịch chuyển bitwise).


5
Bạn đã chạy một điểm chuẩn? Nó chỉ có khoảng một chục dòng mã. Bạn đã học được gì từ việc chạy một điểm chuẩn? [Gợi ý: làm điều đó sẽ nhanh hơn đăng câu hỏi ở đây.]
S.Lott

4
Câu hỏi tuyệt vời, đã tạo ra một số câu trả lời / thảo luận khá thú vị. Cảm ơn :)
stealthawaii

22
Ngay cả khi anh ta đã học được câu trả lời bằng cách chấm điểm thì đó vẫn là một câu hỏi hữu ích và đã tạo ra một số câu trả lời thú vị và hữu ích. Ngoài ra, tôi ước mọi người sẽ tập trung vào vấn đề và không viết câu trả lời và bình luận cho câu trả lời đưa ra lời khuyên không liên quan về việc có nên thực hiện tối ưu hóa trong câu hỏi hay không. Tại sao không cho rằng OP đang đặt câu hỏi như được viết thay vì cho rằng anh ấy hoặc cô ấy 'thực sự' muốn lời khuyên ở quy mô lớn hơn viết lại.
Kevin Whitefoot

1
Phân chia chậm hơn nhiều, so với nhân. Nhưng một số trình biên dịch / VM thông minh biến đổi phép chia thành phép nhân, do đó các bài kiểm tra của bạn sẽ có kết quả giống nhau (cả hai bài kiểm tra phép nhân).
Ivan Kuckir

4
Hơi lạc đề một chút, nhưng tôi chỉ muốn nói rằng tôi đồng ý với @KevinWhite feet bao nhiêu. Không có gì bực bội bằng việc đọc từ các bài giảng hơn là các câu trả lời kỹ thuật cho các câu hỏi kỹ thuật. Cảm ơn Kevin cho nhận xét của bạn!
Jean-François

Câu trả lời:


78

Con trăn

time python -c 'for i in xrange(int(1e8)): t=12341234234.234 / 2.0'
real    0m26.676s
user    0m25.154s
sys     0m0.076s

time python -c 'for i in xrange(int(1e8)): t=12341234234.234 * 0.5'
real    0m17.932s
user    0m16.481s
sys     0m0.048s

nhân nhanh hơn 33%

Lua:

time lua -e 'for i=1,1e8 do t=12341234234.234 / 2.0 end'
real    0m7.956s
user    0m7.332s
sys     0m0.032s

time lua -e 'for i=1,1e8 do t=12341234234.234 * 0.5 end'
real    0m7.997s
user    0m7.516s
sys     0m0.036s

=> không có sự khác biệt thực sự

LuaJIT:

time luajit -O -e 'for i=1,1e8 do t=12341234234.234 / 2.0 end'
real    0m1.921s
user    0m1.668s
sys     0m0.004s

time luajit -O -e 'for i=1,1e8 do t=12341234234.234 * 0.5 end'
real    0m1.843s
user    0m1.676s
sys     0m0.000s

=> nó chỉ nhanh hơn 5%

kết luận: trong Python, nhân nhanh hơn để phân chia, nhưng khi bạn đến gần CPU hơn bằng cách sử dụng các máy ảo hoặc JIT tiên tiến hơn, lợi thế sẽ biến mất. Hoàn toàn có khả năng một VM VM trong tương lai sẽ khiến nó không liên quan


Cảm ơn các mẹo về việc sử dụng lệnh thời gian để điểm chuẩn!
Edmundito

2
Kết luận của bạn là sai. Nó có liên quan nhiều hơn khi JIT / VM trở nên tốt hơn. Việc phân chia trở nên chậm hơn so với chi phí thấp hơn của VM. Hãy nhớ rằng trình biên dịch nói chung không thể tối ưu hóa nhiều dấu phẩy động để đảm bảo độ chính xác.
rasmus

7
@rasmus: Khi JIT trở nên tốt hơn, nó sẽ có khả năng sử dụng một hướng dẫn nhân CPU hơn mặc dù bạn đã yêu cầu phân chia.
Ben Voigt

68

Luôn luôn sử dụng bất cứ điều gì là rõ ràng nhất. Bất cứ điều gì khác bạn làm là cố gắng vượt qua trình biên dịch. Nếu trình biên dịch hoàn toàn thông minh, nó sẽ làm tốt nhất để tối ưu hóa kết quả, nhưng không có gì có thể khiến anh chàng tiếp theo không ghét bạn vì giải pháp bẻ khóa nhảm nhí của bạn (tôi rất thích thao tác bit, nhưng rất vui! )

Tối ưu hóa sớm là gốc rễ của mọi tội lỗi. Luôn nhớ ba quy tắc tối ưu hóa!

  1. Đừng tối ưu hóa.
  2. Nếu bạn là một chuyên gia, hãy xem quy tắc số 1
  3. Nếu bạn là một chuyên gia và có thể biện minh cho nhu cầu, thì hãy sử dụng quy trình sau:

    • Mã nó không được tối ưu hóa
    • xác định mức độ "Đủ nhanh" - Lưu ý yêu cầu / câu chuyện của người dùng yêu cầu số liệu đó.
    • Viết bài kiểm tra tốc độ
    • Kiểm tra mã hiện có - Nếu nó đủ nhanh, bạn đã hoàn thành.
    • Viết lại nó được tối ưu hóa
    • Kiểm tra mã tối ưu. NẾU nó không đáp ứng được số liệu, hãy vứt nó đi và giữ bản gốc.
    • Nếu nó đáp ứng thử nghiệm, hãy giữ mã gốc dưới dạng nhận xét

Ngoài ra, thực hiện những việc như loại bỏ các vòng lặp bên trong khi chúng không cần thiết hoặc chọn danh sách được liên kết qua một mảng để sắp xếp chèn không phải là tối ưu hóa, chỉ là lập trình.


7
đó không phải là trích dẫn Knuth đầy đủ; xem en.wikipedia.org/wiki/ Kẻ
Jason S

Không, có khoảng 40 trích dẫn khác nhau về chủ đề này từ nhiều nguồn khác nhau. Tôi loại một vài mảnh với nhau.
Bill K

Câu cuối cùng của bạn làm cho không rõ khi nào nên áp dụng quy tắc số 1 và số 2, khiến chúng tôi quay lại nơi chúng tôi bắt đầu: Chúng tôi cần quyết định tối ưu hóa nào đáng giá và quy tắc nào không. Giả vờ câu trả lời là hiển nhiên không phải là một câu trả lời.
Matt

2
Nó thực sự khó hiểu với bạn? Luôn áp dụng quy tắc 1 và 2 trừ khi bạn thực sự không đáp ứng các thông số kỹ thuật của máy khách và rất quen thuộc với toàn bộ hệ thống bao gồm các đặc điểm ngôn ngữ và bộ nhớ đệm của CPU. Tại thời điểm đó, CHỈ làm theo quy trình trong 3, đừng nghĩ "Này, nếu tôi lưu trữ biến này cục bộ thay vì gọi một getter, mọi thứ có thể sẽ nhanh hơn. Trước tiên hãy chứng minh rằng nó không đủ nhanh sau đó kiểm tra từng tối ưu hóa riêng biệt và vứt bỏ những thứ không có ích. Tài liệu rất nhiều trên đường đi.
Bill K

49

Tôi nghĩ rằng điều này đang trở nên quá đáng để bạn nên làm bất cứ điều gì làm cho mã dễ đọc hơn. Trừ khi bạn thực hiện các hoạt động hàng ngàn, nếu không phải hàng triệu lần, tôi nghi ngờ bất cứ ai cũng sẽ nhận thấy sự khác biệt.

Nếu bạn thực sự phải lựa chọn, điểm chuẩn là cách duy nhất để đi. Tìm (các) chức năng nào đang cung cấp cho bạn các vấn đề, sau đó tìm ra vị trí xảy ra trong chức năng và khắc phục các phần đó. Tuy nhiên, tôi vẫn nghi ngờ rằng một phép toán đơn lẻ (thậm chí một phép lặp lại nhiều lần, nhiều lần) sẽ là nguyên nhân của bất kỳ nút thắt nào.


1
Khi tôi sử dụng để chế tạo bộ xử lý radar, một thao tác duy nhất đã tạo ra sự khác biệt. Nhưng chúng tôi đã tối ưu hóa mã máy để đạt được hiệu năng thời gian thực. Đối với mọi thứ khác, tôi bỏ phiếu cho đơn giản và rõ ràng.
S.Lott

Tôi đoán đối với một số điều, bạn có thể quan tâm đến một hoạt động. Nhưng tôi hy vọng rằng trong 99% các ứng dụng ngoài kia, điều đó không thành vấn đề.
Thomas Owens

27
Đặc biệt là khi OP đang tìm kiếm một câu trả lời bằng Python. Tôi nghi ngờ bất cứ điều gì cần lượng hiệu quả đó sẽ được viết bằng Python.
Ed S.

4
Một bộ phận có lẽ là hoạt động tốn kém nhất trong một thói quen giao nhau tam giác, là cơ sở cho hầu hết các raytracers. Nếu bạn lưu trữ đối ứng và nhân lên thay vì chia, bạn sẽ trải nghiệm nhiều lần tăng tốc.
solinent

@solinent - vâng, tăng tốc nhưng tôi nghi ngờ "nhiều lần" - phép chia và phép nhân dấu phẩy động không nên khác nhau nhiều hơn khoảng 4: 1, trừ khi bộ xử lý trong câu hỏi thực sự được tối ưu hóa cho phép nhân và không chia.
Jason S

39

Phép nhân nhanh hơn, phép chia chính xác hơn. Bạn sẽ mất một số độ chính xác nếu số của bạn không phải là lũy thừa 2:

y = x / 3.0;
y = x * 0.333333;  // how many 3's should there be, and how will the compiler round?

Ngay cả khi bạn để trình biên dịch tìm ra hằng số đảo ngược đến độ chính xác hoàn hảo, câu trả lời vẫn có thể khác.

x = 100.0;
x / 3.0 == x * (1.0/3.0)  // is false in the test I just performed

Vấn đề tốc độ chỉ có khả năng quan trọng trong các ngôn ngữ C / C ++ hoặc JIT, và thậm chí sau đó chỉ khi hoạt động trong một vòng lặp tại một nút cổ chai.


Phân chia là chính xác nếu bạn chia cho cả số.
plinth

7
Phân chia dấu phẩy động với mẫu số> tử số phải đưa ra các giá trị vô nghĩa trong các bit bậc thấp; phân chia thường làm giảm độ chính xác.
S.Lott

8
@ S.Lott: Không, điều đó không đúng. Tất cả các cài đặt điểm nổi tuân thủ theo chuẩn IEEE-754 phải làm tròn kết quả của mọi hoạt động một cách hoàn hảo (nghĩa là đến số dấu phẩy động gần nhất) đối với chế độ làm tròn hiện tại. Nhân với đối ứng luôn luôn sẽ gây ra nhiều lỗi hơn, ít nhất là vì phải làm tròn thêm một lần nữa.
Electro

1
Tôi biết câu trả lời này đã hơn 8 tuổi, nhưng nó sai lệch; bạn có thể thực hiện phép chia mà không làm mất độ chính xác đáng kể: y = x * (1.0/3.0);và trình biên dịch thường sẽ tính toán 1/3 tại thời gian biên dịch. Vâng, 1/3 không phải là hoàn hảo biểu diễn trong IEEE-754, nhưng khi bạn đang thực hiện dấu chấm động số học bạn đang mất đi độ chính xác nào , cho dù bạn đang làm nhân hoặc chia, vì các bit bậc thấp là tròn. Nếu bạn biết tính toán của mình nhạy cảm với lỗi làm tròn, bạn cũng nên biết cách xử lý vấn đề tốt nhất.
Jason S

1
@JasonS Tôi vừa rời khỏi một chương trình chạy qua đêm, bắt đầu từ 1.0 và đếm lên 1 ULP; Tôi so sánh kết quả của phép nhân (1.0/3.0)với chia cho 3.0. Tôi đã nhận được tới 1,0000036666774155 và trong đó khoảng 7,3% kết quả là khác nhau. Tôi đoán rằng chúng chỉ khác nhau 1 bit, nhưng vì số học của IEEE được đảm bảo làm tròn đến kết quả chính xác gần nhất, tôi đứng trước tuyên bố của mình rằng phép chia chính xác hơn. Cho dù sự khác biệt là đáng kể là tùy thuộc vào bạn.
Đánh dấu tiền chuộc

25

Nếu bạn muốn tối ưu hóa mã của mình nhưng vẫn rõ ràng, hãy thử điều này:

y = x * (1.0 / 2.0);

Trình biên dịch sẽ có thể thực hiện phép chia tại thời gian biên dịch, do đó bạn có được bội số tại thời gian chạy. Tôi hy vọng độ chính xác sẽ giống như trong y = x / 2.0trường hợp.

Trường hợp điều này có thể quan trọng RẤT NHIỀU trong các bộ xử lý nhúng, trong đó cần phải mô phỏng điểm nổi để tính toán số học dấu phẩy động.


12
Phù hợp với bản thân (và bất cứ ai -1'd này) - đó là thông lệ tiêu chuẩn trong thế giới nhúng và các kỹ sư phần mềm trong lĩnh vực đó thấy rõ.
Jason S

4
+1 vì là người duy nhất ở đây nhận ra rằng trình biên dịch không thể tối ưu hóa các hoạt động của dấu phẩy động theo cách chúng muốn. Họ thậm chí không thể thay đổi thứ tự các toán hạng theo cấp số nhân để đảm bảo độ chính xác (trừ khi nó sử dụng chế độ thư giãn).
rasmus

1
OMG, có ít nhất 6 lập trình viên nghĩ rằng toán tiểu học không rõ ràng. Phép nhân AFAIK, IEEE 754 là giao hoán (nhưng không liên kết).
maaartinus

13
Có lẽ bạn đang thiếu điểm. Nó không có gì để làm với tính chính xác đại số. Trong một thế giới lý tưởng, bạn chỉ có thể chia hai: y = x / 2.0;nhưng trong thế giới thực, bạn có thể phải kết hợp trình biên dịch để thực hiện một phép nhân ít tốn kém hơn. Có lẽ nó không rõ ràng tại sao y = x * (1.0 / 2.0);tốt hơn, và thay vào đó sẽ rõ ràng hơn y = x * 0.5;. Nhưng thay đổi 2.0thành một 7.0và tôi muốn nhìn thấy y = x * (1.0 / 7.0);nhiều hơn y = x * 0.142857142857;.
Jason S

3
Điều này thực sự làm rõ lý do tại sao nó dễ đọc hơn (và chính xác) để sử dụng phương pháp của bạn.
Juan Martinez

21

Chỉ cần thêm một cái gì đó cho tùy chọn "ngôn ngữ khác".
C: Vì đây chỉ là một bài tập học thuật thực sự không có gì khác biệt, tôi nghĩ tôi sẽ đóng góp một cái gì đó khác biệt.

Tôi biên dịch để lắp ráp mà không tối ưu hóa và xem kết quả.
Mật mã:

int main() {

    volatile int a;
    volatile int b;

    asm("## 5/2\n");
    a = 5;
    a = a / 2;

    asm("## 5*0.5");
    b = 5;
    b = b * 0.5;

    asm("## done");

    return a + b;

}

biên dịch với gcc tdiv.c -O1 -o tdiv.s -S

chia cho 2:

movl    $5, -4(%ebp)
movl    -4(%ebp), %eax
movl    %eax, %edx
shrl    $31, %edx
addl    %edx, %eax
sarl    %eax
movl    %eax, -4(%ebp)

và phép nhân với 0,5:

movl    $5, -8(%ebp)
movl    -8(%ebp), %eax
pushl   %eax
fildl   (%esp)
leal    4(%esp), %esp
fmuls   LC0
fnstcw  -10(%ebp)
movzwl  -10(%ebp), %eax
orw $3072, %ax
movw    %ax, -12(%ebp)
fldcw   -12(%ebp)
fistpl  -16(%ebp)
fldcw   -10(%ebp)
movl    -16(%ebp), %eax
movl    %eax, -8(%ebp)

Tuy nhiên, khi tôi đổi những cái đó intthành doubles (đó là điều mà con trăn có thể sẽ làm), tôi đã nhận được điều này:

bộ phận:

flds    LC0
fstl    -8(%ebp)
fldl    -8(%ebp)
flds    LC1
fmul    %st, %st(1)
fxch    %st(1)
fstpl   -8(%ebp)
fxch    %st(1)

phép nhân:

fstpl   -16(%ebp)
fldl    -16(%ebp)
fmulp   %st, %st(1)
fstpl   -16(%ebp)

Tôi đã không điểm chuẩn bất kỳ mã nào trong số này, nhưng chỉ bằng cách kiểm tra mã bạn có thể thấy rằng sử dụng số nguyên, phép chia cho 2 ngắn hơn nhân với 2. Sử dụng nhân đôi, phép nhân ngắn hơn vì trình biên dịch sử dụng mã op dấu phẩy động của bộ xử lý, có thể chạy nhanh hơn (nhưng thực ra tôi không biết) hơn là không sử dụng chúng cho cùng một hoạt động. Vì vậy, cuối cùng câu trả lời này đã chỉ ra rằng hiệu suất của phép nhân 0,5 so với chia cho 2 phụ thuộc vào việc triển khai ngôn ngữ và nền tảng mà nó chạy trên đó. Cuối cùng, sự khác biệt là không đáng kể và là điều bạn hầu như không bao giờ phải lo lắng, ngoại trừ về khả năng đọc.

Là một lưu ý phụ, bạn có thể thấy rằng trong chương trình của tôi main()trở lại a + b. Khi tôi mang từ khóa không ổn định đi, bạn sẽ không bao giờ đoán được phần lắp ráp trông như thế nào (không bao gồm cài đặt chương trình):

## 5/2

## 5*0.5
## done

movl    $5, %eax
leave
ret

nó đã thực hiện cả phép chia, phép nhân và phép cộng trong một lệnh đơn! Rõ ràng bạn không phải lo lắng về điều này nếu trình tối ưu hóa là bất kỳ loại đáng kính nào.

Xin lỗi vì câu trả lời quá dài.


1
Đây không phải là một "hướng dẫn duy nhất". Nó chỉ được gấp lại liên tục.
kvanberendonck

5
@kvanberendonck Tất nhiên đó là một hướng dẫn duy nhất. Đếm chúng: movl $5, %eax Tên của tối ưu hóa không quan trọng hoặc thậm chí có liên quan. Bạn chỉ muốn được hạ mình trên một câu trả lời bốn năm tuổi.
Carson Myers

2
Bản chất của tối ưu hóa vẫn rất quan trọng để hiểu, bởi vì nó nhạy cảm theo ngữ cảnh: Nó chỉ áp dụng nếu bạn thêm / nhân / chia / v.v. các hằng số thời gian biên dịch, trong đó trình biên dịch có thể thực hiện tất cả các phép toán trước và chuyển câu trả lời cuối cùng vào một thanh ghi khi chạy. Phép chia chậm hơn rất nhiều so với phép nhân trong trường hợp chung (ước số thời gian chạy), nhưng tôi cho rằng nhân với số đối ứng chỉ giúp nếu bạn chia cho cùng một mẫu số nhiều hơn một lần. Bạn có thể biết tất cả những điều đó, nhưng các lập trình viên mới hơn có thể cần nó đánh vần, vì vậy ... chỉ trong trường hợp.
Mike S

10

Thứ nhất, trừ khi bạn đang làm việc trong C hoặc ASSEMBLY, có lẽ bạn đang ở một ngôn ngữ cấp cao hơn, nơi các bộ nhớ và tổng phí gọi chung sẽ hoàn toàn làm giảm sự khác biệt giữa nhân và chia cho điểm không liên quan. Vì vậy, chỉ cần chọn những gì đọc tốt hơn trong trường hợp đó.

Nếu bạn đang nói ở mức rất cao, nó sẽ không thể chậm hơn đối với bất cứ điều gì bạn có khả năng sử dụng nó cho. Bạn sẽ thấy trong các câu trả lời khác, mọi người cần thực hiện một triệu nhân / chia chỉ để đo một số chênh lệch dưới một phần nghìn giây giữa hai phần.

Nếu bạn vẫn tò mò, từ quan điểm tối ưu hóa ở mức độ thấp:

Phân chia có xu hướng có một đường ống dài hơn đáng kể so với nhân. Điều này có nghĩa là sẽ mất nhiều thời gian hơn để có được kết quả, nhưng nếu bạn có thể khiến bộ xử lý bận rộn với các tác vụ không phụ thuộc, thì nó sẽ không khiến bạn phải trả nhiều hơn nhiều lần.

Bao lâu sự khác biệt đường ống là hoàn toàn phụ thuộc vào phần cứng. Phần cứng cuối cùng tôi sử dụng là khoảng 9 chu kỳ cho nhân FPU và 50 chu kỳ cho phân chia FPU. Nghe có vẻ nhiều, nhưng sau đó bạn sẽ mất 1000 chu kỳ cho việc bỏ lỡ bộ nhớ, do đó có thể đưa mọi thứ vào quan điểm.

Một sự tương tự là đặt một chiếc bánh trong lò vi sóng trong khi bạn xem một chương trình TV. Tổng thời gian bạn đưa bạn ra khỏi chương trình TV là thời gian đặt nó vào lò vi sóng và lấy nó ra khỏi lò vi sóng. Thời gian còn lại bạn vẫn xem chương trình TV. Vì vậy, nếu chiếc bánh mất 10 phút để nấu thay vì 1 phút, thì nó thực sự không sử dụng hết thời gian xem tv của bạn nữa.

Trong thực tế, nếu bạn sẽ đạt đến mức độ quan tâm về sự khác biệt giữa Nhân và Chia, bạn cần hiểu các đường ống, bộ đệm, quầy hàng chi nhánh, dự đoán không theo thứ tự và phụ thuộc đường ống. Nếu điều này không giống như nơi bạn dự định đi với câu hỏi này, thì câu trả lời chính xác là bỏ qua sự khác biệt giữa hai câu hỏi.

Nhiều (nhiều) năm trước, việc tránh chia và luôn luôn sử dụng bội số là rất quan trọng, nhưng hồi đó, các lần truy cập bộ nhớ ít liên quan hơn và các phân chia còn tệ hơn nhiều. Ngày nay tôi đánh giá mức độ dễ đọc cao hơn, nhưng nếu không có sự khác biệt về khả năng đọc, tôi nghĩ rằng đó là một thói quen tốt để lựa chọn bội số.


7

Viết bất cứ điều gì được nêu rõ hơn ý định của bạn.

Sau khi chương trình của bạn hoạt động, hãy tìm ra những gì chậm, và làm cho nó nhanh hơn.

Đừng làm theo cách khác.


6

Làm bất cứ điều gì bạn cần. Trước tiên hãy nghĩ đến độc giả của bạn, đừng lo lắng về hiệu suất cho đến khi bạn chắc chắn rằng bạn có vấn đề về hiệu suất.

Hãy để trình biên dịch thực hiện hiệu suất cho bạn.


5

Nếu bạn đang làm việc với các số nguyên hoặc các loại dấu phẩy động, đừng quên các toán tử bẻ khóa của bạn: << >>

    int y = 10;
    y = y >> 1;
    Console.WriteLine("value halved: " + y);
    y = y << 1;
    Console.WriteLine("now value doubled: " + y);

7
tối ưu hóa này được tự động thực hiện phía sau hậu trường trong bất kỳ trình biên dịch hiện đại nào.
Dustin Getz

Có ai đã kiểm tra nếu kiểm tra (sử dụng bit op) nếu toán hạng (?) Có phiên bản có thể thay đổi để sử dụng thay thế không? hàm mul (a, b) {if (b là 2) trả về << 1; if (b là 4) trả về << 2; // ... vv trả về a * b; } Tôi đoán là IF quá đắt nên sẽ kém hiệu quả hơn.
Christopher Lightfoot

Điều đó đã không in bất cứ nơi nào gần với những gì tôi tưởng tượng; Đừng bận tâm.
Christopher Lightfoot

Đối với các hoạt động const, một trình biên dịch bình thường sẽ thực hiện công việc; Nhưng ở đây chúng tôi đang sử dụng python nên tôi không chắc nó có đủ thông minh để biết không? (Nó nên như vậy).
Christopher Lightfoot

Phím tắt tốt, ngoại trừ việc nó không ngay lập tức rõ ràng những gì đang thực sự xảy ra. Hầu hết các lập trình viên thậm chí không nhận ra các toán tử bithift.
Blazemonger

4

Trên thực tế có một lý do chính đáng là như một quy tắc chung của phép nhân ngón tay cái sẽ nhanh hơn sự phân chia. Phân chia điểm nổi trong phần cứng được thực hiện bằng thuật toán trừ và dịch chuyển có điều kiện ("phân chia dài" với số nhị phân) hoặc - nhiều khả năng hiện nay - với các lần lặp như thuật toán của Goldschmidt . Sự thay đổi và phép trừ cần ít nhất một chu kỳ trên một bit chính xác (các lần lặp gần như không thể song song hóa như sự thay đổi và cộng của phép nhân) và các thuật toán lặp thực hiện ít nhất một phép nhân cho mỗi lần lặp. Trong cả hai trường hợp, rất có khả năng bộ phận sẽ mất nhiều chu kỳ hơn. Tất nhiên điều này không tính đến các quirks trong trình biên dịch, di chuyển dữ liệu hoặc độ chính xác. Tuy nhiên, nhìn chung, nếu bạn đang mã hóa một vòng lặp bên trong trong phần nhạy cảm về thời gian của chương trình, thì viết0.5 * xhay 1.0/2.0 * xđúng hơn x / 2.0là một điều hợp lý để làm. Phương pháp sư phạm của "mã rõ ràng nhất" là hoàn toàn đúng, nhưng cả ba điều này đều rất gần với khả năng dễ đọc đến nỗi trường hợp này trong trường hợp này chỉ là mô phạm.


3

Tôi đã luôn học được rằng phép nhân hiệu quả hơn.


"Hiệu quả" là từ sai. Đúng là hầu hết các bộ xử lý nhân nhanh hơn so với phân chia. Tuy nhiên, với các kiến ​​trúc đường ống hiện đại, chương trình của bạn có thể thấy không có sự khác biệt. Như nhiều người khác đang nói, thì thật là cách tốt hơn hết chỉ cần làm những gì tốt nhất để đọc một con người.
TED

3

Phép nhân thường nhanh hơn - chắc chắn không bao giờ chậm hơn. Tuy nhiên, nếu nó không phải là tốc độ quan trọng, hãy viết bất cứ điều gì rõ ràng nhất.


2

Phân chia điểm nổi (nói chung) đặc biệt chậm, vì vậy trong khi phép nhân dấu phẩy động cũng tương đối chậm, có lẽ nó nhanh hơn phân chia điểm nổi.

Nhưng tôi có xu hướng trả lời "điều đó không thực sự quan trọng", trừ khi hồ sơ đã chỉ ra rằng sự phân chia là một chút tắc nghẽn so với phép nhân. Tuy nhiên, tôi đoán rằng việc lựa chọn phép nhân so với phép chia sẽ không có tác động lớn đến hiệu suất trong ứng dụng của bạn.


2

Điều này trở thành câu hỏi nhiều hơn khi bạn đang lập trình lắp ráp hoặc có lẽ C. Tôi cho rằng với hầu hết các ngôn ngữ hiện đại, việc tối ưu hóa như điều này đang được thực hiện cho tôi.


2

Hãy cảnh giác với "phép nhân đoán thường tốt hơn nên tôi cố gắng tuân theo điều đó khi tôi viết mã".

Trong bối cảnh của câu hỏi cụ thể này, tốt hơn ở đây có nghĩa là "nhanh hơn". Mà không hữu ích lắm.

Suy nghĩ về tốc độ có thể là một sai lầm nghiêm trọng. Có những hàm ý lỗi sâu sắc trong dạng đại số cụ thể của phép tính.

Xem số học dấu phẩy động với phân tích lỗi . Xem các vấn đề cơ bản trong phân tích lỗi và số học dấu phẩy động .

Trong khi một số giá trị dấu phẩy động là chính xác, hầu hết các giá trị dấu phẩy động là một xấp xỉ; chúng là một số giá trị lý tưởng cộng với một số lỗi. Mọi thao tác áp dụng cho giá trị lý tưởng và giá trị lỗi.

Những vấn đề lớn nhất đến từ việc cố gắng thao túng hai con số gần bằng nhau. Hầu hết các bit bên phải (các bit lỗi) chiếm ưu thế trong kết quả.

>>> for i in range(7):
...     a=1/(10.0**i)
...     b=(1/10.0)**i
...     print i, a, b, a-b
... 
0 1.0 1.0 0.0
1 0.1 0.1 0.0
2 0.01 0.01 -1.73472347598e-18
3 0.001 0.001 -2.16840434497e-19
4 0.0001 0.0001 -1.35525271561e-20
5 1e-05 1e-05 -1.69406589451e-21
6 1e-06 1e-06 -4.23516473627e-22

Trong ví dụ này, bạn có thể thấy rằng khi các giá trị trở nên nhỏ hơn, sự khác biệt giữa các số gần bằng nhau tạo ra kết quả khác không trong đó câu trả lời đúng là 0.


1

Tôi đã đọc ở đâu đó rằng phép nhân hiệu quả hơn trong C / C ++; Không có ý tưởng liên quan đến các ngôn ngữ được giải thích - sự khác biệt có lẽ là không đáng kể do tất cả các chi phí khác.

Trừ khi nó trở thành một vấn đề gắn bó với những gì dễ bảo trì / dễ đọc hơn - tôi ghét nó khi mọi người nói với tôi điều này nhưng nó rất đúng.


1

Tôi sẽ đề xuất phép nhân nói chung, bởi vì bạn không phải trải qua các chu kỳ đảm bảo rằng số chia của bạn không bằng 0. Tất nhiên, điều này không áp dụng nếu số chia của bạn là một hằng số.


1

Java android, được mô tả sơ lược về Samsung GT-S5830

public void Mutiplication()
{
    float a = 1.0f;

    for(int i=0; i<1000000; i++)
    {
        a *= 0.5f;
    }
}
public void Division()
{
    float a = 1.0f;

    for(int i=0; i<1000000; i++)
    {
        a /= 2.0f;
    }
}

Các kết quả?

Multiplications():   time/call: 1524.375 ms
Division():          time/call: 1220.003 ms

Phân chia nhanh hơn khoảng 20% ​​so với phép nhân (!)


1
Để thực tế, bạn nên kiểm tra a = i*0.5, không a *= 0.5. Đó là cách hầu hết các lập trình viên sẽ sử dụng các hoạt động.
Blazemonger

1

Như với bài viết số 24 (nhân nhanh hơn) và # 30 - nhưng đôi khi cả hai đều dễ hiểu như sau:

1*1e-6F;

1/1e6F;

~ Tôi thấy cả hai đều dễ đọc và phải lặp lại hàng tỷ lần. Vì vậy, rất hữu ích khi biết rằng phép nhân thường nhanh hơn.


1

Có một sự khác biệt, nhưng nó phụ thuộc vào trình biên dịch. Lúc đầu trên vs2003 (c ++) tôi không có sự khác biệt đáng kể nào đối với loại kép (điểm nổi 64 bit). Tuy nhiên, khi chạy thử nghiệm một lần nữa vào vs2010, tôi đã phát hiện ra một sự khác biệt rất lớn, nhanh hơn gấp 4 lần cho phép nhân. Theo dõi điều này, có vẻ như vs2003 và vs2010 tạo ra mã fpu khác nhau.

Trên Pentium 4, 2,8 GHz, vs2003:

  • Phép nhân: 8,09
  • Sư đoàn: 7.97

Trên Xeon W3530, vs2003:

  • Phép nhân: 4,68
  • Sư đoàn: 4,64

Trên Xeon W3530, vs2010:

  • Phép nhân: 5,33
  • Sư đoàn: 21,05

Có vẻ như trên vs2003, một phép chia trong một vòng lặp (vì vậy số chia được sử dụng nhiều lần) đã được dịch thành phép nhân với phép nghịch đảo. Vào vs2010, tối ưu hóa này không được áp dụng nữa (tôi cho rằng vì có một chút kết quả khác nhau giữa hai phương thức). Cũng lưu ý rằng cpu thực hiện phân chia nhanh hơn ngay khi tử số của bạn là 0,0. Tôi không biết thuật toán chính xác được gắn trong chip, nhưng có lẽ nó phụ thuộc vào số lượng.

Chỉnh sửa 18-03-2013: quan sát cho vs2010


Tôi tự hỏi nếu có bất kỳ lý do nào trình biên dịch không thể thay thế, ví dụ như n/10.0với một biểu thức của biểu mẫu (n * c1 + n * c2)? Tôi hy vọng rằng trên hầu hết các bộ xử lý, một phép chia sẽ mất nhiều thời gian hơn hai phép nhân và phép chia và tôi tin rằng phép chia theo bất kỳ hằng số nào có thể mang lại kết quả làm tròn chính xác trong mọi trường hợp sử dụng công thức được chỉ định.
supercat

1

Đây là một câu trả lời thú vị ngớ ngẩn:

x / 2.0không tương đương với x * 0,5

Giả sử bạn đã viết phương pháp này vào ngày 22 tháng 10 năm 2008.

double half(double x) => x / 2.0;

Bây giờ, 10 năm sau bạn biết rằng bạn có thể tối ưu hóa đoạn mã này. Phương pháp này được tham chiếu trong hàng trăm công thức trong suốt ứng dụng của bạn. Vì vậy, bạn thay đổi nó, và trải nghiệm cải thiện hiệu suất 5% đáng chú ý.

double half(double x) => x * 0.5;

Đó có phải là quyết định đúng đắn để thay đổi mã? Trong toán học, hai biểu thức thực sự là tương đương. Trong khoa học máy tính, điều đó không phải lúc nào cũng đúng. Vui lòng đọc Tối thiểu hóa ảnh hưởng của các vấn đề chính xác để biết thêm chi tiết. Nếu các giá trị được tính toán của bạn - tại một số điểm - so với các giá trị khác, bạn sẽ thay đổi kết quả của các trường hợp cạnh. Ví dụ:

double quantize(double x)
{
    if (half(x) > threshold))
        return 1;
    else
        return -1;
}

Kết quả là; một khi bạn giải quyết cho một trong hai, sau đó gắn bó với nó!


1
Downvote? Làm thế nào về một bình luận giải thích suy nghĩ của bạn? Câu trả lời này chắc chắn có liên quan 100%.
l33t

Trong khoa học máy tính, nhân / chia các giá trị dấu phẩy động bằng lũy ​​thừa 2 là không mất, trừ khi giá trị trở nên không chuẩn hóa hoặc tràn.
Soonts

Vì điểm nổi không mất mát tại thời điểm phân chia, điều đó không thực sự quan trọng nếu tuyên bố của bạn là đúng. Mặc dù tôi sẽ rất ngạc nhiên nếu có.
l33t

1
Điểm nổi không phải là không mất mát tại thời điểm phân chia chỉ khi bạn đang xây dựng với trình biên dịch cổ phát ra mã x87 không dùng nữa. Trên phần cứng hiện đại, chỉ có biến số float / double là không mất, 32 hoặc 64 bit IEEE 754: en.wikipedia.org/wiki/IEEE_754 Do cách thức hoạt động của IEEE 754, khi bạn chia cho 2 hoặc nhân 0,5, bạn giảm số mũ của 1, phần còn lại của các bit (dấu + mantissa) không thay đổi. Và cả hai 20.5số có thể được biểu diễn chính xác trong IEEE 754, mà không làm mất độ chính xác (không giống như 0.4, hoặc 0.1chúng không thể).
Soonts

0

Chà, nếu chúng ta giả sử rằng một hoạt động thêm / subtrack có chi phí 1, thì nhân chi phí 5 và chia chi phí khoảng 20.


Bạn lấy những con số này từ đâu? kinh nghiệm? cảm giac tôt? bài viết trên mạng? Làm thế nào họ sẽ thay đổi cho các loại dữ liệu khác nhau?
kroiz

0

Sau một cuộc thảo luận dài và thú vị như vậy, đây là ý kiến ​​của tôi về vấn đề này: Không có câu trả lời cuối cùng cho câu hỏi này. Như một số người chỉ ra, nó phụ thuộc vào cả hai, phần cứng (cf piotrkgast128 ) và trình biên dịch ( các bài kiểm tra của cf @Javier ). Nếu tốc độ không quan trọng, nếu ứng dụng của bạn không cần xử lý lượng dữ liệu khổng lồ theo thời gian thực, bạn có thể chọn sử dụng phân chia trong khi nếu tốc độ xử lý hoặc tải bộ xử lý là một vấn đề, thì phép nhân có thể là an toàn nhất. Cuối cùng, trừ khi bạn biết chính xác nền tảng nào mà ứng dụng của bạn sẽ được triển khai, điểm chuẩn là vô nghĩa. Và để rõ ràng về mã, một nhận xét sẽ thực hiện công việc!


-3

Về mặt kỹ thuật không có thứ gọi là phép chia, chỉ có phép nhân với các phần tử nghịch đảo. Ví dụ: Bạn không bao giờ chia cho 2, trên thực tế bạn nhân 0,5.

'Phân chia' - chúng ta hãy tự nhủ rằng nó tồn tại trong một giây - luôn khó hơn phép nhân vì để 'chia' xcho ymột người trước tiên cần tính toán giá trị y^{-1}sao cho y*y^{-1} = 1sau đó thực hiện phép nhân x*y^{-1}. Nếu bạn đã biết y^{-1}thì không tính toán nó yphải là một tối ưu hóa.


3
Mà hoàn toàn bỏ qua thực tế của cả hai lệnh hiện có trong silicon.
NPSF3000

@ NPSF3000 - Tôi không theo dõi. Theo giả định rằng cả hai hoạt động tồn tại, nó chỉ đơn giản khẳng định rằng hoạt động phân chia hoàn toàn liên quan đến việc tính toán nghịch đảo nhân và nhân, sẽ luôn khó hơn so với chỉ thực hiện một phép nhân. Các silicon là một chi tiết thực hiện.
satnhak

@ BTyler. Nếu cả hai lệnh tồn tại trong silicon và cả hai lệnh đều có cùng số chu kỳ [như người ta mong đợi] so với mức độ tương đối phức tạp thì các hướng dẫn hoàn toàn không liên quan đến POV hiệu suất.
NPSF3000

@ NPSF3000 - nhưng cả hai đều không thực hiện cùng một số chu kỳ vì chúng nhân nhanh hơn.
satnhak
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.