Lười biếng
Nó không phải là "tối ưu hóa trình biên dịch", nhưng nó là thứ được đảm bảo bởi đặc tả ngôn ngữ, vì vậy bạn luôn có thể tin tưởng vào nó đang diễn ra. Về cơ bản, điều này có nghĩa là công việc không được thực hiện cho đến khi bạn "làm điều gì đó" với kết quả. (Trừ khi bạn làm một trong nhiều điều để cố tình tắt sự lười biếng.)
Rõ ràng, đây là toàn bộ chủ đề theo đúng nghĩa của nó và SO đã có rất nhiều câu hỏi và câu trả lời về nó.
Theo kinh nghiệm hạn chế của tôi, làm cho mã của bạn quá lười biếng hoặc quá khắt khe có bao la hình phạt hiệu suất lớn hơn (trong thời gian và không gian) hơn bất kỳ những thứ khác tôi sắp nói về ...
Phân tích nghiêm ngặt
Lười biếng là về việc tránh công việc trừ khi nó cần thiết. Nếu trình biên dịch có thể xác định rằng một kết quả nhất định sẽ "luôn luôn" là cần thiết, thì nó sẽ không bận tâm đến việc lưu trữ phép tính và thực hiện nó sau này; nó sẽ chỉ thực hiện nó trực tiếp, bởi vì nó hiệu quả hơn. Điều này được gọi là "phân tích nghiêm ngặt".
Rõ ràng, gotcha là trình biên dịch không thể luôn luôn phát hiện khi một cái gì đó có thể được thực hiện nghiêm ngặt. Đôi khi bạn cần đưa ra gợi ý nhỏ cho trình biên dịch. (Tôi không biết bất kỳ cách dễ dàng nào để xác định xem phân tích nghiêm ngặt có thực hiện được những gì bạn nghĩ hay không, ngoài việc lội qua đầu ra Core.)
Nội tuyến
Nếu bạn gọi một hàm và trình biên dịch có thể cho biết bạn đang gọi hàm nào, nó có thể cố gắng "nội tuyến" hàm đó - nghĩa là, để thay thế lệnh gọi hàm bằng một bản sao của chính hàm đó. Chi phí hoạt động của một lệnh gọi thường khá nhỏ, nhưng nội tuyến thường cho phép các tối ưu hóa khác xảy ra, điều này sẽ không xảy ra nếu không, do đó, nội tuyến có thể là một chiến thắng lớn.
Các hàm chỉ được nội tuyến nếu chúng "đủ nhỏ" (hoặc nếu bạn thêm một pragma đặc biệt yêu cầu nội tuyến). Ngoài ra, các hàm chỉ có thể được nội tuyến nếu trình biên dịch có thể cho biết bạn đang gọi hàm nào. Có hai cách chính mà trình biên dịch không thể nói:
Nếu chức năng bạn đang gọi được chuyển đến từ một nơi khác. Ví dụ: khi filter
hàm được biên dịch, bạn không thể nội tuyến vị ngữ bộ lọc, bởi vì đó là đối số do người dùng cung cấp.
Nếu hàm bạn đang gọi là một phương thức lớp và trình biên dịch không biết loại nào có liên quan. Ví dụ, khi sum
hàm được biên dịch, trình biên dịch không thể nội tuyến +
hàm, bởi vì sum
hoạt động với một số loại số khác nhau, mỗi loại có một +
chức năng khác nhau .
Trong trường hợp sau, bạn có thể sử dụng {-# SPECIALIZE #-}
pragma để tạo các phiên bản của hàm được mã hóa cứng thành một loại cụ thể. Ví dụ, {-# SPECIALIZE sum :: [Int] -> Int #-}
sẽ biên dịch một phiên bản sum
mã hóa cứng cho Int
loại, có nghĩa là+
có thể được nội tuyến trong phiên bản này.
Tuy nhiên, xin lưu ý rằng sum
chức năng đặc biệt mới của chúng tôi sẽ chỉ được gọi khi trình biên dịch có thể cho biết chúng tôi đang làm việc với Int
. Nếu không, bản gốc, đa hình sum
được gọi. Một lần nữa, chức năng gọi thực tế là khá nhỏ. Đó là những tối ưu bổ sung mà nội tuyến có thể cho phép có lợi.
Loại bỏ phổ biến phụ
Nếu một khối mã nhất định tính toán cùng một giá trị hai lần, trình biên dịch có thể thay thế nó bằng một thể hiện duy nhất của cùng một tính toán. Ví dụ, nếu bạn làm
(sum xs + 1) / (sum xs + 2)
sau đó trình biên dịch có thể tối ưu hóa điều này thành
let s = sum xs in (s+1)/(s+2)
Bạn có thể mong đợi rằng trình biên dịch sẽ luôn luôn làm điều này. Tuy nhiên, rõ ràng trong một số tình huống, điều này có thể dẫn đến hiệu suất kém hơn, không tốt hơn, vì vậy GHC không phải lúc nào cũng làm điều này. Thành thật mà nói, tôi không thực sự hiểu các chi tiết đằng sau cái này. Nhưng điểm mấu chốt là, nếu sự chuyển đổi này quan trọng với bạn, không khó để thực hiện thủ công. (Và nếu nó không quan trọng, tại sao bạn lại lo lắng về nó?)
Biểu thức tình huống
Hãy xem xét những điều sau đây:
foo (0:_ ) = "zero"
foo (1:_ ) = "one"
foo (_:xs) = foo xs
foo ( []) = "end"
Ba phương trình đầu tiên đều kiểm tra xem danh sách này có trống không (trong số những thứ khác). Nhưng kiểm tra ba lần điều tương tự là lãng phí. May mắn thay, trình biên dịch rất dễ dàng tối ưu hóa điều này thành một số biểu thức trường hợp lồng nhau. Trong trường hợp này, một cái gì đó như
foo xs =
case xs of
y:ys ->
case y of
0 -> "zero"
1 -> "one"
_ -> foo ys
[] -> "end"
Điều này là khá ít trực quan, nhưng hiệu quả hơn. Bởi vì trình biên dịch có thể dễ dàng thực hiện việc chuyển đổi này, bạn không phải lo lắng về nó. Chỉ cần viết kết hợp mẫu của bạn theo cách trực quan nhất có thể; trình biên dịch rất tốt trong việc sắp xếp lại và sắp xếp lại thứ này để làm cho nó nhanh nhất có thể.
Dung hợp
Thành ngữ Haskell tiêu chuẩn để xử lý danh sách là xâu chuỗi các chức năng lấy một danh sách và tạo ra một danh sách mới. Ví dụ kinh điển là
map g . map f
Thật không may, trong khi sự lười biếng đảm bảo bỏ qua công việc không cần thiết, tất cả các phân bổ và thỏa thuận cho hiệu suất sap danh sách trung gian. "Hợp nhất" hoặc "phá rừng" là nơi trình biên dịch cố gắng loại bỏ các bước trung gian này.
Vấn đề là, hầu hết các chức năng này là đệ quy. Nếu không có đệ quy, nó sẽ là một bài tập cơ bản để sắp xếp tất cả các hàm thành một khối mã lớn, chạy trình giả lập trên nó và tạo ra mã thực sự tối ưu không có danh sách trung gian. Nhưng vì sự đệ quy, điều đó sẽ không hiệu quả.
Bạn có thể sử dụng các {-# RULE #-}
pragma để sửa một số điều này. Ví dụ,
{-# RULES "map/map" forall f g xs. map f (map g xs) = map (f.g) xs #-}
Bây giờ mỗi khi GHC thấy map
được áp dụng map
, nó sẽ đưa nó vào một danh sách duy nhất trong danh sách, loại bỏ danh sách trung gian.
Rắc rối là, điều này chỉ hoạt động để map
theo sau map
. Có nhiều khả năng khác - map
theo sau filter
, filter
tiếp theo map
, v.v. Thay vì mã hóa bằng tay một giải pháp cho mỗi trong số chúng, cái gọi là "hợp hạch dòng" đã được phát minh. Đây là một thủ thuật phức tạp hơn, mà tôi sẽ không mô tả ở đây.
Cái dài và ngắn của nó là: Đây đều là những thủ thuật tối ưu hóa đặc biệt được viết bởi lập trình viên . Bản thân GHC không biết gì về phản ứng tổng hợp; đó là tất cả trong các thư viện danh sách và các thư viện container khác. Vì vậy, những gì tối ưu hóa xảy ra phụ thuộc vào cách các thư viện container của bạn được viết (hoặc, thực tế hơn, những thư viện bạn chọn sử dụng).
Ví dụ: nếu bạn làm việc với mảng Haskell '98, đừng mong đợi bất kỳ sự hợp nhất nào. Nhưng tôi hiểu rằng vector
thư viện có khả năng hợp nhất rộng rãi. Đó là tất cả về các thư viện; trình biên dịch chỉ cung cấp RULES
pragma. (Nhân tiện, nó cực kỳ mạnh mẽ. Là một tác giả thư viện, bạn có thể sử dụng nó để viết lại mã máy khách!)
Meta:
Tôi đồng ý với những người nói "mã đầu tiên, hồ sơ thứ hai, tối ưu hóa thứ ba".
Tôi cũng đồng ý với những người nói rằng "thật hữu ích khi có một mô hình tinh thần cho một quyết định thiết kế nhất định có chi phí bao nhiêu".
Cân bằng trong mọi thứ, và tất cả những điều đó ...