2013-09-01 45 views
5

Theo như kiến ​​thức tối ưu (giới hạn) của tôi về tối ưu hóa, tôi luôn nghĩ rằng tối ưu hóa trình biên dịch không quan trọng là số nhiều. Ý tôi là, chúng ta chắc chắn có thể đạt được một vài phần trăm (có lẽ là 0 ... 100% như là một suy nghĩ sơ bộ rất thô sơ) về thời gian bằng cách sử dụng thanh ghi thay vì bộ nhớ và bỏ vòng, nhưng yếu tố cơ bản làm hạn chế hiệu suất của mã thực sự là sự lựa chọn của các thuật toán.Mã chạy nhanh hơn khi kích hoạt tối ưu hóa. Tui bỏ lỡ điều gì vậy?


Gần đây tôi đã bắt đầu một dự án thú cưng - một ngôn ngữ kịch bản nhỏ được giải thích. Nó biên dịch sang bytecode và thực hiện nó bằng cách sử dụng một máy ảo. Để dễ dàng gỡ lỗi, lược tả và thử nghiệm, trước tiên tôi đã tự nhiên biên dịch mã bằng các cờ -O0 -g của gcc (và clang), sau đó với -O2. Tôi đã sau đó time 'da chương trình nhỏ chạy trên máy ảo, mà về cơ bản thực hiện điều này (pseudo-code, tôi không hiển thị cho bạn các cú pháp thực tế cho đến khi dự án đi công cộng):

i = 1 
sum = 0 

while (i < 10000000) { 
    sum = (sum + i * i) mod 1000000 
    i++ 
} 

print sum 

này dịch đại khái để lắp ráp giả sau đây:

load r0, 0 # sum = 0 
load r1, 1 # i = 1 
load r2, 1000000 
load r3, 10000000 

loop: 
mul r4, r1, r1 # i * i 
add r0, r0, r4 # sum += i * i 
mod r0, r0, r2 # sum %= 1000000 
inc r1 
gt r5, r1, r3 # if i > 10000000 
jz r5, loop # then don't goto loop 

ret r0 

Về cơ bản, đây là vòng lặp chặt chẽ với 10000.000 lần lặp. time báo cáo rằng nó chạy 0,47 ... 0,52 giây khi được biên dịch với -O2, trong 1,51 ... 1,74 giây khi được biên dịch với -O0 -g và trong 3,16 ... 3,47 giây khi lược tả (-pg) cũng được bật.

Như bạn có thể thấy, có sự khác biệt 7 lần giữa thời gian thực hiện nhanh nhất và chậm nhất.

Điều này tự nó không phải là đáng ngạc nhiên, bởi vì tôi biết rằng thông tin gỡ lỗi bổ sung và thiếu tối ưu hóa nhỏ thực sự làm cho mã chạy chậm hơn, nhưng bây giờ là phần thú vị. Để có một nhận thức thực tế hơn về những gì thực sự xảy ra, tôi đã chơi cùng một trò chơi với Lua 5.2. Quan trọng xung quanh với Makefile, tôi đã tìm thấy rằng chương trình Lua rất giống nhau:

local sum = 0 

for i = 1, 10000000 do 
    sum = (sum + i * i) % 1000000 
end 

print(sum) 

chạy trong khoảng 0,8 ... 0,87 giây khi Lua được biên soạn với -O0 -g -pg, và trong 0,39 ... 0,43 giây khi chỉ -O2 được bật. Vì vậy, mã của tôi dường như có tốc độ tăng gấp 7 lần khi trình tối ưu hóa thực hiện các sửa lỗi phức tạp, trong khi việc triển khai tham chiếu Lua dường như được hưởng lợi từ những cải tiến này ít hơn nhiều.

Bây giờ câu hỏi của tôi là:

  1. Bất cứ ý tưởng tại sao điều này xảy ra? Tôi nghi ngờ rằng lý do chính là những người sáng tạo của Lua thông minh hơn tôi và biết rõ hơn về trình biên dịch và máy ảo của họ đang làm gì.

  2. Tôi cũng bắt gặp suy nghĩ của mình "điều này phải được tối ưu hóa sớm, hãy để tôi chỉ chuyển nó cho trình tối ưu hóa, nó sẽ thực hiện công việc" vài lần. Chúng bao gồm các hàm tĩnh một dòng được gọi trong việc thực hiện hầu hết các lệnh VM (tôi nghĩ rằng chúng sẽ được inlined khi cần), sử dụng các biểu thức phức tạp khác nhau (đôi khi với các hiệu ứng phụ không dễ dự đoán) có thể được đơn giản hóa, mặc dù.Thái độ này của tôi có được tính không? (Và đó có phải là thái độ đúng đắn không?)

  3. Và dù sao, tôi có nên lo lắng về hiện tượng này không? Mã của tôi chạy chậm hơn 1,5 lần so với Lua, sau khi tất cả - và đó là khá tốt (ít nhất là nó đủ tốt cho tôi). Tôi có nên cố gắng cải thiện hiệu suất của việc xây dựng gỡ lỗi chỉ vì không làm như vậy chỉ ra rằng tôi thiếu kiến ​​thức thân mật của mã của riêng tôi? Hoặc có thể tôi chỉ quên nó hoàn toàn xa như việc phát hành (tối ưu) xây dựng là đủ nhanh?

(Nếu câu hỏi này sẽ là một sự phù hợp tốt hơn cho Prog.SE hoặc CodeReview, cho tôi biết và tôi sẽ di chuyển nó.)

+4

7x không phải là thứ tự cường độ nhanh hơn trong thế giới tập trung thập phân. –

+0

Bạn đã xem mã lắp ráp cho vòng lặp của mình với cờ -O0 và -O2 chưa? Nó sẽ giải thích rất nhiều (tôi nghĩ rằng gcc thậm chí không sử dụng sổ đăng ký tích cực với -O0). Đối với tôi, tôi sẽ không lo lắng nhiều về tốc độ của mã được biên dịch mà không tối ưu hóa. Về nội tuyến: nó phụ thuộc vào trình biên dịch, và tôi đồng ý với bạn: suy nghĩ của nó là tối ưu hóa sớm, hồ sơ mã cuối cùng của bạn và chơi với nội tuyến sau đó. – Inspired

+0

Thật khó để nói mà không có nhiều chi tiết hơn. Tôi cũng nghi ngờ điểm số 1 của bạn có liên quan. Trình biên dịch sẽ làm những việc như di chuyển vòng lặp bất biến ra khỏi vòng lặp, loại bỏ mã chết, sắp xếp lại các câu lệnh để kết hợp bộ nhớ cache tốt hơn và thời gian bộ nhớ, v.v.Có thể (thậm chí có khả năng) rằng những người sáng tạo của Lua rất ý thức về những hiệu ứng này và ghi nhớ chúng bằng máy ảo của riêng chúng. –

Trả lời

1

Thứ nhất, khẳng định của bạn rằng các thuật toán là quan trọng hơn so với các tối ưu hóa nói chung là đúng, nhưng cùng một thuật toán có thể được mã hóa để sử dụng tốt nhất hoặc tệ nhất nền tảng bạn đang thực thi, vì vậy tối ưu hóa luôn luôn được xem xét ... chỉ cần tránh tối ưu hóa sớm, thay vì tránh tối ưu hóa.

Tiếp theo, hãy nhớ rằng việc gỡ lỗi xây dựng thêm rất nhiều chi phí. Nhiều hơn là chỉ vô hiệu hóa tối ưu hóa. Để xem những gì trình tối ưu hóa đang làm, hãy sử dụng bản phát hành bản phát hành có tối ưu hóa bị tắt.

Sự khác biệt giữa Ngôn ngữ và ngôn ngữ của bạn sẽ do hiệu quả của trình thông dịch bytecode của bạn. Một sự kém hiệu quả nhỏ ở đây sẽ có ảnh hưởng lớn đến tốc độ thực hiện vòng lặp lớn như vậy. bạn cũng có thể thêm các tối ưu hóa như:

  • sử dụng "thanh ghi" để giữ biến được sử dụng trong vòng lặp (trong mã byte, tải biến vào vị trí 1, sau đó sử dụng hướng dẫn nhân và mod mới các khe bằng cách sử dụng chỉ mục mảng đơn giản thay vì biến được đặt tên, sau đó ghi giá trị cuối cùng của vị trí trở lại biến vào cuối vòng lặp. cách bạn có thể diễn tả điều này trong byecode để biến vòng lặp và logic được thực thi bằng mã nguyên gốc thay vì bằng cách giải thích bytecode.Bạn rõ ràng có thể thêm các trường hợp đặc biệt cho nhiều tình huống, do đó, mẹo ở đây là tìm ra các cấu trúc phổ biến nhất và tối ưu hóa những người đầu tiên, để có được tiếng nổ lớn nhất cho buck của bạn.

Cuối cùng, đừng lo lắng về hiệu quả của mã gỡ lỗi của bạn. Khi bạn có một thông dịch viên làm việc, sau đó bạn có thể lập cấu hình một bản phát hành bản phát hành để tìm các khu vực mà bạn có thể cải thiện. Làm điều này sớm sẽ không giúp ích gì, vì không có điểm tối ưu hóa một phần mã hoàn chỉnh và sau đó phát hiện ra rằng nó cần phải được thay đổi để hỗ trợ một tính năng mới. Và chỉ khi bạn có một hệ thống làm việc mà bạn có thể bắt đầu viết các script điển hình sẽ thực hiện phiên dịch của bạn theo cách thực tế - bạn có thể thấy rằng việc tối ưu hóa một vòng lặp giống như ví dụ trên không mang lại lợi ích nào trong các kịch bản hàng ngày.

+0

Chắc chắn, tôi không muốn "tránh tối ưu hóa chút nào", đoạn cuối cùng của bạn là câu trả lời thực sự tôi nghĩ. Ngoài ra, tôi không sử dụng các biến được đặt tên. Nếu bạn nhìn vào "lắp ráp" tôi đăng, thật dễ dàng để suy luận rằng tôi đã sử dụng một máy đăng ký. Tuy nhiên, việc phát hiện số đếm lặp liên tục là một ý tưởng hay. –