7

Tôi đang thử nghiệm với giao diện chức năng nước ngoài trong Haskell. Tôi muốn thực hiện một bài kiểm tra đơn giản để xem tôi có thể làm đệ quy lẫn nhau không. Vì vậy, tôi đã tạo mã Haskell sau đây:Biên dịch tối ưu hóa cuộc gọi đuôi trong đệ quy lẫn nhau trên C và Haskell

module MutualRecursion where 
import Data.Int 

foreign import ccall countdownC::Int32->IO() 
foreign export ccall countdownHaskell::Int32->IO() 

countdownHaskell::Int32->IO() 
countdownHaskell n = print n >> if n > 0 then countdownC (pred n) else return() 

Lưu ý rằng trường hợp đệ quy là cuộc gọi đếm ngượcC, vì vậy điều này cần phải đệ quy đuôi.

Trong mã C của tôi, tôi có

#include <stdio.h> 

#include "MutualRecursionHaskell_stub.h" 

void countdownC(int count) 
{ 
    printf("%d\n", count); 
    if(count > 0) 
     return countdownHaskell(count-1); 
} 

int main(int argc, char* argv[]) 
{ 
    hs_init(&argc, &argv); 

    countdownHaskell(10000); 

    hs_exit(); 
    return 0; 
} 

Đó là tương tự như vậy đuôi đệ quy. Vì vậy, sau đó tôi thực hiện một

MutualRecursion: MutualRecursionHaskell_stub 
    ghc -O2 -no-hs-main MutualRecursionC.c MutualRecursionHaskell.o -o MutualRecursion 
MutualRecursionHaskell_stub: 
    ghc -O2 -c MutualRecursionHaskell.hs 

và biên dịch với make MutualRecursion.

Và ... khi chạy, nó sẽ được phân tách sau khi in 8991. Cũng giống như một bài kiểm tra để đảm bảo gcc chính nó có thể xử lý TCO trong đệ quy lẫn nhau, tôi đã làm

void countdownC2(int); 

void countdownC(int count) 
{ 
    printf("%d\n", count); 
    if(count > 0) 
     return countdownC2(count-1); 
} 

void countdownC2(int count) 
{ 
    printf("%d\n", count); 
    if(count > 0) 
     return countdownC(count-1); 
} 

và làm việc khá tốt. Nó cũng hoạt động trong trường hợp đệ quy đơn lẻ chỉ trong C và chỉ trong Haskell.

Vì vậy, câu hỏi của tôi là, có cách nào để chỉ ra cho GHC rằng cuộc gọi đến hàm C bên ngoài có đuôi đệ quy không? Tôi giả định rằng khung ngăn xếp đến từ cuộc gọi từ Haskell đến C và không phải là cách khác xung quanh, vì mã C là rất rõ ràng một sự trở lại của một cuộc gọi chức năng.

Trả lời

3

Tôi tin rằng các cuộc gọi đuôi C-Haskell đa ngôn ngữ là rất, rất khó đạt được.

Tôi không biết chi tiết chính xác, nhưng thời gian chạy C và thời gian chạy Haskell là bao la khác nhau. Các yếu tố chính cho sự khác biệt này, như xa như tôi có thể thấy, bao gồm:

  • mô hình khác nhau: hoàn toàn chức năng vs bắt buộc
  • thu gom rác thải vs quản lý thủ công bộ nhớ
  • ngữ nghĩa lười biếng vs nghiêm ngặt một

Các loại tối ưu hóa có khả năng tồn tại trên các ranh giới ngôn ngữ cho những khác biệt như vậy là bên cạnh số không. Có lẽ, về mặt lý thuyết, người ta có thể phát minh ra một thời gian chạy quảng cáo C cùng với thời gian chạy Haskell để một số tối ưu hóa là khả thi, nhưng GHC và GCC không được thiết kế theo cách này.

Chỉ cần để hiển thị một ví dụ về sự khác biệt tiềm năng, giả sử chúng ta có đoạn mã sau Haskell

p :: Int -> Bool 
p x = x==42 

main = if p 42 
     then putStrLn "A"  -- A 
     else putStrLn "B"  -- B 

Một thi thể của main có thể là như sau:

  • push địa chỉ của A trên ngăn xếp
  • đẩy địa chỉ của B trên ngăn xếp
  • mủ h 42 trên stack
  • nhảy để p
  • A: print "A", nhảy đến kết thúc
  • B: print "B", nhảy đến kết thúc

khi p được thực hiện như sau:

  • p: pop x từ stack
  • pop b từ đống
  • pop a từ đống
  • kiểm tra x chống lại 42
  • nếu bằng nhau, nhảy đến a
  • nhảy để b

Lưu ý cách p được gọi với hai địa chỉ trở lại, một cho mỗi kết quả có thể. Điều này khác với C, có triển khai chuẩn chỉ sử dụng một địa chỉ trả về. Khi vượt qua ranh giới, trình biên dịch phải tính toán sự khác biệt này và bù đắp.

Ở trên tôi cũng không tính đến trường hợp khi đối số của p là một phần nhỏ, để giữ cho nó đơn giản. Người cấp phát GHC cũng có thể kích hoạt thu gom rác thải.

Lưu ý rằng triển khai thực hiện hư cấu ở trên đã thực sự được sử dụng trong quá khứ bởi GHC (máy STG được gọi là "đẩy/nhập"). Ngay cả khi bây giờ nó không còn được sử dụng nữa, máy "STG/áp dụng" STG chỉ gần với thời gian chạy C. Tôi thậm chí không chắc chắn về GHC bằng cách sử dụng stack C thường xuyên: Tôi nghĩ rằng nó không, bằng cách sử dụng riêng của mình.

Bạn có thể kiểm tra GHC developer wiki để xem chi tiết đẫm máu.

+0

Có cách nào để ngăn các vị trí trả lại kép không? Ví dụ, tôi đã viết một thói quen thay thế bằng cách sử dụng mô hình phù hợp (đối với 0 cho trường hợp cơ sở), nhưng điều đó không giúp đỡ. Nói chung, có cách nào tel tel bảo GHC biên dịch theo một cách để cho phép đệ quy đuôi qua ranh giới? – Crazycolorz5

+0

@ Crazycolorz5 Điều chỉnh thời gian chạy là một nhiệm vụ rất khó khăn. Bạn dường như tin rằng đó là GHC nên thích ứng với thời gian chạy của nó theo chuẩn C. Để tìm ra khó khăn như thế nào, suy nghĩ về cách khác xung quanh: sửa đổi GCC để cho phép cho nhiều lợi nhuận, bộ sưu tập rác, vv Đó là bên cạnh không thể. Những gì bạn yêu cầu là rất, rất khó đạt được với GHC hiện tại, hoặc thậm chí trong tương lai xa như tôi có thể thấy. – chi

0

Trong khi tôi không có chuyên gia trong Haskel-C interop, tôi không tưởng tượng một cuộc gọi từ C đến Haskel có thể là một lời gọi hàm thẳng - nó rất có thể phải đi qua trung gian để thiết lập môi trường. Kết quả là, cuộc gọi của bạn để haskel thực sự sẽ bao gồm các cuộc gọi đến trung gian này. Cuộc gọi này có thể được tối ưu hóa bởi gcc. Nhưng cuộc gọi từ trung gian đến thói quen Haskel thực sự không được tối ưu hóa một cách đáng kể - vì vậy tôi cho rằng, đây là những gì bạn đang xử lý. Bạn có thể kiểm tra đầu ra lắp ráp để đảm bảo.

Các vấn đề liên quan