11

Tôi đang làm việc trên một ngôn ngữ trung gian và một máy ảo để chạy một ngôn ngữ chức năng với một vài thuộc tính "có vấn đề":Một số tối ưu hóa rõ ràng cho một máy ảo thực hiện một ngôn ngữ chức năng là gì?

namespace
  • từ vựng (đóng cửa)
  • phát triển động gọi stack
  • Một loại số nguyên chậm (bignums)

Ngôn ngữ trung gian dựa trên ngăn xếp, với bảng băm đơn giản cho không gian tên hiện tại. Chỉ cần để bạn có được một ý tưởng về những gì có vẻ như, đây là McCarthy91 chức năng:

# McCarthy 91: M(n) = n - 10 if n > 100 else M(M(n + 11)) 
.sub M 
    args 
    sto n 

    rcl n 
    float 100 
    gt 
    .if 
     .sub 
      rcl n 
      float 10 
      sub 
     .end 
     .sub 
      rcl n 
      float 11 
      add 
      list 1 
      rcl M 
      call-fast 
      list 1 
      rcl M 
      tail 
     .end 
    call-fast 
.end 

Các "vòng lặp lớn" là đơn giản:

  • lấy một lệnh
  • tăng con trỏ lệnh (hoặc chương trình truy cập)
  • đánh giá giảng dạy

Cùng với sto, rcl và nhiều thứ khác, có ba hướng dẫn chức năng cuộc gọi:

  • call bản không gian tên (bản sao sâu) và đẩy con trỏ hướng dẫn vào các cuộc gọi stack
  • call-fast là tương tự, nhưng chỉ tạo ra một bản sao cạn
  • tail cơ bản là một 'goto'

Việc thực hiện thực sự đơn giản. Để cung cấp cho bạn một ý tưởng tốt hơn, đây chỉ là một đoạn mã ngẫu nhiên từ giữa "vòng lặp lớn" (cập nhật, xem dưới đây)

} else if inst == 2 /* STO */ { 
     local[data] = stack[len(stack) - 1] 
     if code[ip + 1][0] != 3 { 
      stack = stack[:len(stack) - 1] 
     } else { 
      ip++ 
     } 
    } else if inst == 3 /* RCL */ { 
     stack = append(stack, local[data]) 
    } else if inst == 12 /* .END */ { 
     outer = outer[:len(outer) - 1] 
     ip = calls[len(calls) - 1] 
     calls = calls[:len(calls) - 1] 
    } else if inst == 20 /* CALL */ { 
     calls = append(calls, ip) 
     cp := make(Local, len(local)) 
     copy(cp, local) 
     outer = append(outer, &cp) 
     x := stack[len(stack) - 1] 
     stack = stack[:len(stack) - 1] 
     ip = x.(int) 
    } else if inst == 21 /* TAIL */ { 
     x := stack[len(stack) - 1] 
     stack = stack[:len(stack) - 1] 
     ip = x.(int) 

Vấn đề là thế này: Gọi McCarthy91 16 lần với giá trị là -10.000 mất , gần như không tạo ra sự khác biệt, 3 giây (sau khi tối ưu hóa bản sao sâu, bổ sung gần một giây).

Câu hỏi của tôi là: Một số kỹ thuật phổ biến để tối ưu hóa việc diễn giải loại ngôn ngữ này là gì? Có trái cây treo thấp nào không?

Tôi đã sử dụng các lát cho danh sách của mình (đối số, các ngăn xếp khác nhau, lát bản đồ cho các không gian tên, ...), vì vậy tôi làm điều này ở mọi nơi: call_stack[:len(call_stack) - 1]. Ngay bây giờ, tôi thực sự không có một đầu mối những gì đoạn mã làm cho chương trình này chậm. Mọi lời khuyên sẽ được đánh giá cao, mặc dù tôi chủ yếu đang tìm kiếm các chiến lược tối ưu hóa chung.

Bên cạnh:

Tôi có thể giảm thời gian thực hiện một chút bằng cách phá vỡ quy ước gọi điện của tôi. Lệnh list <n> tìm nạp n đối số của ngăn xếp và đẩy danh sách trở lại vào ngăn xếp, lệnh args bật ra khỏi danh sách đó và đẩy từng mục trở lại ngăn xếp. Đây là trước hết là kiểm tra các hàm được gọi với số lượng đối số chính xác và thứ hai để có thể gọi hàm với các danh sách đối số biến (tức là(defun f x:xs)). Loại bỏ điều đó, và cũng có thể thêm một hướng dẫn sto* <x>, thay thế sto <x>; rcl <x>, tôi có thể làm cho nó xuống đến 2 giây. Vẫn không rực rỡ, và tôi phải có công việc này list/args. :)

Một sang một bên (đây là một câu hỏi dài tôi biết, xin lỗi):

Profiling chương trình với pprof nói với tôi rất ít (Tôi mới to Go trong trường hợp đó không rõ ràng) :-). Đây là 3 mặt hàng đầu theo báo cáo của pprof:

16 6.1% 6.1%  16 6.1% sweep    pkg/runtime/mgc0.c:745 
    9 3.4% 9.5%  9 3.4% fmt.(*fmt).fmt_qc pkg/fmt/format.go:323 
    4 1.5% 13.0%  4 1.5% fmt.(*fmt).integer pkg/fmt/format.go:248 

Đây là những thay đổi mà tôi đã thực hiện cho đến nay:

  • tôi loại bỏ các bảng băm. Thay vào đó, bây giờ tôi chỉ chuyển các con trỏ tới các mảng và tôi chỉ sao chép hiệu quả phạm vi cục bộ khi tôi phải thực hiện.
  • Tôi đã thay thế tên lệnh bằng các mã nguyên dương. Trước đây, tôi đã lãng phí khá nhiều thời gian so sánh các chuỗi.
  • Các hướng dẫn call-fast đã biến mất (tăng tốc là không thể đo lường được nữa sau khi thay đổi khác)
  • Thay vì phải "int", "phao" và "str" ​​hướng dẫn, tôi chỉ có một eval và tôi đánh giá hằng tại thời gian biên dịch (biên dịch bytecode). Sau đó, eval chỉ cần đẩy một tham chiếu đến chúng.
  • Sau khi thay đổi ngữ nghĩa của .if, tôi có thể loại bỏ các hàm giả này. bây giờ là .if, .else.endif, với ngầm gotos và khối ngữ nghĩa tương tự như .sub. (some example code)

Sau khi triển khai trình biên dịch lexer, parser và bytecode, tốc độ giảm xuống một chút, nhưng không quá khủng khiếp. Tính toán MC (-10000) 16 lần giúp nó đánh giá 4,2 triệu lệnh bytecode trong 1,2 giây. Đây là a sample of the code it generates (từ this).

The whole thing is on github

+4

Vui lòng không sử dụng hashtables và bất kỳ loại tra cứu tên nào khác cho các ngôn ngữ phạm vi từ vựng! Nó không có ý nghĩa gì cả. Trình biên dịch của bạn có thể cấp phát thanh ghi tĩnh. Rất dễ suy ra môi trường đã được thiết lập cho mỗi trừu tượng lambda. –

+0

Tôi chưa thực sự hiểu ý của bạn. Bạn có thể roi lên một ví dụ ngắn, nói cho "rcl" và "sto"? –

+2

Sử dụng các khe số cho các đối số, biến và các biến đóng. Giới thiệu opcodes như 'ldarg N', 'starg N', 'ldloc N', 'stloc N', 'ldenv N', 'stenv N'. Giải quyết tên biến thành các số khi biên dịch. Xây dựng các danh sách biến bị bắt bằng cách so sánh các danh sách biến miễn phí và các biến ràng buộc cho mỗi điểm trừu tượng lambda. Giới thiệu một hướng dẫn đặc biệt để xây dựng một thể hiện đóng cửa (nên tương tự như một cuộc gọi với một danh sách các biến được bắt). Đó là cách hầu hết các ngôn ngữ chức năng được triển khai (cả VM lẫn native). Nó có thể rất hiệu quả. –

Trả lời

8

Có hàng chục năm nghiên cứu về điều bạn có thể tối ưu hóa:

+0

Liên kết thứ hai này thực sự là một cái gì đó, cảm ơn bạn rất nhiều! –

+1

@StefanoPalazzo bạn cũng có thể xem làm thế nào để tối ưu hóa CPS, vì đây là những gì nhiều trình biên dịch đang sử dụng dưới mui xe. –

4

Bạn nên có cơ quan đại diện thuật toán hiệu quả cho các khái niệm khác nhau của thông dịch viên của bạn. Làm các bản sao sâu trên một cái nhìn có thể bắt đầu giống như một ý tưởng khủng khiếp, nhưng tôi thấy rằng bạn đã loại bỏ nó.

(Có, hoạt động ngăn xếp ngăn xếp của bạn bằng cách sử dụng lát cắt có vẻ đáng ngờ. Bạn nên đảm bảo chúng thực sự có độ phức tạp thuật toán mong muốn hoặc sử dụng cấu trúc dữ liệu chuyên dụng (... ngăn xếp). sử dụng cấu trúc dữ liệu tất cả các mục đích như danh sách Python hoặc hashtables PHP cho việc sử dụng này, vì chúng không nhất thiết được thiết kế để xử lý trường hợp sử dụng cụ thể này, nhưng có thể lát cắt đảm bảo chi phí đẩy và giảm O (1) mọi hoàn cảnh.)

Cách tốt nhất để xử lý môi trường, miễn là không sử dụng số liệu thay vì biến (chỉ số de Bruijn (0 cho biến bị ràng buộc cuối cùng), hoặc cấp độ Bruijn (0 cho biến bị ràng buộc đầu tiên) Bằng cách này bạn chỉ có thể giữ một mảng được điều chỉnh kích thước động cho môi trường và truy cập vào nó rất nhanh, nếu bạn đóng cửa lớp học đầu tiên, bạn cũng cần phải chụp môi trường. tốn kém: bạn phải sao chép một phần của nó trong một cấu trúc chuyên dụng, hoặc sử dụng một cấu trúc không thể thay đổi được cho toàn bộ môi trường. đóng cửa xây dựng là tốt hơn so với việc có một cấu trúc bất biến với nhiều sổ sách kế toán tất cả các thời gian, tất nhiên bạn nên làm cho một phân tích sử dụng để chỉ nắm bắt các biến cần thiết trong các bao đóng của bạn.

Cuối cùng, một khi bạn đã nhổ đi những nguồn không hiệu quả liên quan đến lựa chọn thuật toán của bạn, khu vực nóng sẽ là:

  • thu gom rác thải (chắc chắn là một chủ đề khó khăn, nếu bạn không muốn trở thành một Chuyên gia GC, bạn nên xem xét nghiêm túc việc sử dụng lại thời gian chạy hiện tại); bạn có thể đang sử dụng GC của ngôn ngữ máy chủ của bạn (phân bổ đống trong ngôn ngữ thông dịch của bạn được dịch thành phân bổ đống trong ngôn ngữ triển khai của bạn, với cùng thời gian), không rõ ràng trong đoạn mã bạn đã hiển thị; chiến lược này là tuyệt vời để có được một cái gì đó hợp lý hiệu quả một cách đơn giản

  • thực hiện số học; có tất cả các loại hacks có hiệu quả khi các số nguyên bạn thao tác thực tế là nhỏ. Đặt cược tốt nhất của bạn là sử dụng lại công việc của những người đã đầu tư tấn nỗ lực vào việc này, vì vậy tôi khuyên bạn nên sử dụng lại ví dụ the GMP library. Sau đó, một lần nữa, bạn cũng có thể tái sử dụng hỗ trợ ngôn ngữ máy chủ của bạn cho bignum nếu nó có một số, trong trường hợp của bạn Go's math/big gói.

  • thiết kế cấp thấp của vòng thông dịch viên của bạn. Trong một ngôn ngữ với "bytecode đơn giản" như của bạn (mỗi lệnh bytecode được dịch trong một số ít các lệnh gốc, trái ngược với các bytecode phức tạp có ngữ nghĩa cấp cao như Parrot bytecode), thực tế "looping và dispatching by bytecode "mã có thể là một nút cổ chai. Đã có khá nhiều nghiên cứu về cách tốt nhất để viết các vòng điều phối bytecode như vậy, để tránh thác của if/then/else (nhảy bảng), hưởng lợi từ dự đoán nhánh bộ xử lý máy chủ, đơn giản hóa luồng điều khiển, v.v. Điều này được gọi là threaded code và có rất nhiều kỹ thuật khác nhau (khá đơn giản): luồng trực tiếp, luồng gián tiếp ... Nếu bạn muốn xem xét một số nghiên cứu, chẳng hạn như Anton Ertl, The Structure and Performance of Efficient Interpreters vào năm 2003 và sau Context threading: A flexible and efficient dispatch technique for virtual machine interpreters. Những lợi ích của những kỹ thuật này có xu hướng khá nhạy cảm với bộ vi xử lý, vì vậy bạn nên tự mình kiểm tra các khả năng khác nhau.

Trong khi công việc STG là thú vị (và cuốn sách Peyton-Jones về triển khai ngôn ngữ lập trình là tuyệt vời), nó phần nào được định hướng để đánh giá lười biếng. Về thiết kế của bytecode hiệu quả cho các ngôn ngữ chức năng nghiêm ngặt, tham chiếu của tôi là công việc của Xavier Leroy năm 1990 trên máy ZINC: The ZINC experiment: An Economical Implementation of the ML Language, đó là công việc đột phá để thực hiện ngôn ngữ ML, và vẫn được sử dụng trong việc thực hiện ngôn ngữ OCaml : có cả một bytecode và một trình biên dịch bản địa, nhưng bytecode vẫn sử dụng một máy ZINC được tôn vinh.

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