2009-07-08 32 views
8

Tôi có một bảng băm trong đó các khóa là danh sách khá phức tạp, với danh sách con của biểu tượng và số nguyên, và giá trị sẽ được sửa đổi tùy thuộc vào giá trị đã có. Bảng được tạo với :test #'equal.Làm cách nào để tôi có thể sử dụng lại tra cứu gethash trong Common Lisp?

tôi làm điều gì đó tương tự như sau rất nhiều:

(defun try-add (i) 
    (let ((old-i (gethash complex-list table nil))) 
    (if (may-add old-i) 
     (push i (gethash complex-list table))))) 

Profiling cho thấy equal kiểm tra mất rất nhiều thời gian. Tôi có ý tưởng tối ưu hóa, rằng số lượng tra cứu gethash có thể được giảm từ hai xuống một. Nó có thể được thực hiện bằng C++ bằng cách sử dụng lại trình lặp, nhưng không chắc chắn làm thế nào điều này sẽ được thực hiện trong Lisp. Ý tưởng nào?

Trả lời

10

Đừng làm bất cứ điều gì đặc biệt, vì việc triển khai thực hiện điều đó cho bạn.

Tất nhiên, phương pháp này là triển khai cụ thể và hiệu suất bảng băm khác nhau giữa các lần triển khai. (Nhưng sau đó các câu hỏi tối ưu hóa luôn được thực hiện cụ thể.)

Câu trả lời sau đây dành cho SBCL. Tôi khuyên bạn nên kiểm tra xem các bảng băm của Lisp có thực hiện cùng một tối ưu hóa hay không. Khiếu nại với nhà cung cấp của bạn nếu họ không làm như vậy!

Điều gì xảy ra trong SBCL là bảng băm lưu trữ chỉ mục bảng cuối cùng được truy cập bởi GETHASH.

Khi PUTHASH (hoặc tương đương, (SETF GETHASH)) được gọi, nó sẽ kiểm tra đầu tiên cho dù chìa khóa ở đó chỉ số cache là EQ để chìa khóa mà bạn đang đi qua trong.

Nếu vậy, toàn bộ bảng băm thói quen tra cứu được thông qua và PUTHASH lưu trữ trực tiếp tại chỉ mục được lưu trong bộ nhớ cache.

Lưu ý rằng EQ chỉ là so sánh con trỏ và do đó cực kỳ nhanh - nó không phải đi qua danh sách nào cả.

Vì vậy, trong ví dụ mã của bạn, hoàn toàn không có phí.

+0

Tuyệt vời - cảm ơn :) –

+0

Có vẻ như chúng ta có thể cảm ơn Paul F. Dietz: http://git.boinkor.net/gitweb/sbcl.git/commitdiff/bc1783335d78be988465e4fc7cf9c5fdb88a3fa4 –

0

Một số cách giải quyết có thể là:

Nếu mô hình chung là tra cứu -> tìm-nó -> ghi đè-nó, sau đó bạn có thể thay thế các loại giá trị cho một danh sách có chứa các loại giá trị. Sau đó, sau khi tìm đối tượng giá trị cho khóa, chỉ cần thay thế phần tử đầu tiên của nó một cách triệt để, ví dụ:

(defun try-add (i) 
    (let ((old-i-list (gethash complex-list table nil))) 
    (if (may-add (first old-i-list)) 
     (setf (first old-i-list) i)      ; overwrite without searching again 
     (setf (gethash complex-list table) (list i))))) ; not there? too bad, we have to gethash again 

Ngoài ra, nếu mẫu chung giống như tra cứu -> không-không-có -> bổ sung, bạn có thể muốn tự băm phím và sau đó sử dụng bảng băm giá trị băm làm khóa. Điều này có thể phức tạp hơn, tùy thuộc vào độ sâu và ngữ nghĩa của các danh sách phức tạp này. Trong trường hợp đơn giản, bạn có thể lấy đi một hàm băm (đệ quy) xor giá trị băm của các phần tử của đối số danh sách của nó.


EDITED: trả lời các câu hỏi trong các ý kiến: ý tưởng là thay vì các phím lập bản đồ bảng băm để các giá trị, các bảng băm bây giờ sẽ lập bản đồ phím vào danh sách yếu tố duy nhất, nơi yếu tố là giá trị. Sau đó, bạn có thể thay đổi nội dung của các danh sách này mà không cần chạm vào bảng băm. Sau đây là từ SBCL:

* (defparameter *my-hash* (make-hash-table)) 
*MY-HASH* 

* (setf (gethash :my-key *my-hash*) (list "old-value")) 
("old-value") 

* (gethash :my-key *my-hash*) 
("old-value") 
T 

* (defparameter old-value-container (gethash :my-key *my-hash*)) 
OLD-VALUE-CONTAINER 

* (setf (first old-value-container) "new value") 
"new value" 

* (gethash :my-key *my-hash*) 
("new value") 
T 
+0

Tôi đã thử một cái gì đó tương tự như mã nguồn bạn đã đăng, nhưng khi thực hiện (setf (first-i-list) ...), nó chỉ thay đổi cũ-i-list và thay đổi không được phản ánh trong băm giá trị bảng. Tôi có hiểu lầm điều gì đó cơ bản không? –

+0

@kotlinski: Nếu bạn đã làm điều đó, nơi giá trị ban đầu của old-i-list là không, thì có, điều đó sẽ không được phản ánh trong giá trị trong bảng băm. Tuy nhiên, nếu bạn đã có một danh sách đã có, sau đó gethash trả về danh sách và bạn có thể thay đổi nó theo cách bạn đang nghĩ đến. Lưu ý, "đẩy" sẽ không hoạt động vì điều đó ảnh hưởng đến biến mà bạn đang đẩy lên, thêm đầu mới và đặt biến để trỏ đến giá trị mới đó. Sau đó nó sẽ chia sẻ một phần của danh sách với giá trị hashtable (giả sử không phải là nil), nhưng không giống nhau. – khedron

+1

"Lisp thường được xây dựng trong cấu trúc dữ liệu nổi tiếng là mờ đục." -- Ý anh là gì? – skypher

0

Một điều bạn có thể làm là sử dụng defstruct để tạo ra một giá trị mà mỗi mục trong bảng điểm của bạn để băm. Danh sách các giá trị của bạn (mà bạn đang đẩy vào trong ví dụ hiện tại của bạn) có thể được lưu trữ bên trong đó. Việc tạo cấu trúc có thể được thực hiện trong lệnh gọi gethash ban đầu đó (làm giá trị mặc định), hoặc có thể được thực hiện thủ công nếu bạn quan sát không có giá trị ở đó. Sau đó, đối tượng có thể bị tác động phụ theo cách bạn đang thực hiện.

(Điều này bỏ qua câu hỏi liệu bạn có thực sự muốn sử dụng các giá trị phức tạp như các khóa có thể bắt đầu của bạn hay không, hoặc nếu có cách để làm việc đó. danh sách phức tạp như các khóa của bạn, và sau đó bạn có thể sử dụng một EQ hashtable thay thế. Nhưng điều đó phụ thuộc rất nhiều vào những gì bạn đang làm.)

0

"Profiling cho thấy rằng kiểm tra bằng nhau mất một thời gian dài."

Có, nhưng bạn đã xác minh rằng # 'EQUAL tra cứu bảng băm cũng mất nhiều thời gian?

Bạn đã biên soạn điều này cho tốc độ trên trình biên dịch tối ưu hóa như SBCL và xem các ghi chú của trình biên dịch chưa?

Sau khi đã giải quyết hai câu hỏi này, bạn cũng có thể thử một bảng băm lồng nhau cho mỗi cấp của các khóa danh sách của bạn. Nó không phải là khó để viết một macro cho các bảng băm tùy ý lồng nhau.

0

Có lẽ tôi là thiếu cái gì rõ ràng, nhưng:

(defun try-add (i) 
    (let ((old-i (gethash complex-list table))) 
    (when (may-add old-i) 
     (push i old-i)))) 

từ:

  • nil đã là mặc định cho GETHASH
  • GETHASH kéo ra toàn bộ đối tượng vì vậy bạn chỉ có thể sửa đổi tại chỗ thay vì yêu cầu PUSH cách tìm lại nó
  • (điểm kiểu: sử dụng WHEN thay vì NẾU khi không có mệnh đề khác)

Chỉnh sửa: oops, tôi là: Tôi đã bỏ lỡ trường hợp cũ-i là không. Nhưng nếu đó không phải là trường hợp phổ biến, thì nó vẫn có thể là một chiến thắng, vì bạn chỉ cần thực hiện tra cứu trong trường hợp đó:

(defun try-add (i) 
    (let ((old-i (gethash complex-list table))) 
    (when (may-add old-i) 
     (if old-i 
     (push i old-i) 
     (push i (gethash complex-list table)))))) 

Hmm, có hiệu quả không?

+0

Không, không. Bạn đang đẩy các mục vào vị trí 'cũ 'mà không ảnh hưởng đến những gì được lưu trữ trong vị trí' (gethash ...) ', vì các danh sách Lisp không rỗng là các con trỏ tới một nút đầu và không chứa các thùng chứa. – Kaz

1

Bạn thực sự có thể truy cập bảng băm ba lần. Tại sao? Vì macro push có thể mở rộng thành mã thực hiện gethash để lấy danh sách và sau đó một số hoạt động system::sethash để lưu trữ giá trị.

Trong vấn đề này, bạn đang kiểm tra giá trị của một địa điểm, đó là danh sách. Nếu danh sách đó thỏa mãn một số thử nghiệm vị ngữ, thì bạn đẩy thứ gì đó vào vị trí đó.

Vấn đề này có thể bị tấn công bằng cách tạo điều hành chuyên dùng mà chụp ngữ nghĩa này:

(push-if <new-value> <predicate> <place>) 

Ví dụ:

(push-if i #'may-add (gethash complex-list table)) 

push-if này được định nghĩa là một macro trong đó sử dụng các get-setf-expansion chức năng trên đối số biểu mẫu <place> để nhận các phần cần thiết để tạo mã để truy cập địa điểm đó chỉ một lần.

Mã được tạo sẽ đánh giá biểu mẫu tải để lấy giá trị cũ từ vị trí, sau đó áp dụng điều kiện cho giá trị cũ và nếu nó thành công, thì nó sẽ chuẩn bị giá trị mới trong biến lưu trữ tạm thời thích hợp thu được từ get-setf-expansion và đánh giá biểu mẫu lưu trữ.

Đây là cách tốt nhất bạn có thể thực hiện trong Lisp di động và bạn có thể thấy rằng điều này vẫn thực hiện hai thao tác băm, như đã đề cập ở trên. (Trong trường hợp này, bạn hy vọng có tối ưu hóa bộ nhớ đệm khá tốt trong bảng băm. Nhưng ít nhất nó là hai phần mềm.)

Cách tiếp cận sẽ được tối ưu hóa như được xây dựng ở dạng biến đổi: incf, push , rotatef, v.v. push-if của chúng tôi sẽ ngang hàng với các bản dựng sẵn.

Nếu nó vẫn hút (thực hiện hai băm để cập nhật vị trí băm, không có tối ưu hóa bộ nhớ đệm), thì cách duy nhất để khắc phục điều đó ở cấp độ triển khai.

push-if đang sau: mở rộng

(defmacro push-if (new-value predicate-fun list-place &environment env) 
    (multiple-value-bind (temp-syms val-forms 
         store-vars store-form access-form) 
         (get-setf-expansion list-place env) 
    (let ((old-val (gensym))) 
     (when (rest store-vars) 
     (error "PUSH-IF: cannot take ref of multiple-value place")) 
     `(multiple-value-bind (,@temp-syms) (values ,@val-forms) 
     (let ((,old-val ,access-form)) 
      (when (funcall ,predicate-fun ,old-val) 
      (setf ,(first store-vars) (cons ,new-value ,old-val)) 
      ,store-form)))))) 

mẫu:

> (macroexpand '(push-if new test place)) 
(LET* ((#:VALUES-12731 (MULTIPLE-VALUE-LIST (VALUES)))) 
(LET ((#:G12730 PLACE)) 
    (WHEN (FUNCALL TEST #:G12730) (SETF #:NEW-12729 (CONS NEW #:G12730)) 
    (SETQ PLACE #:NEW-12729)))) ; 

Trông lành mạnh đối với trường hợp đơn giản khi nơi này là một biến. Chỉ có một vấn đề nhỏ mà tôi sẽ không sửa chữa: các hình thức new, testplace được đánh giá chỉ một lần, nhưng không phải theo thứ tự từ trái sang phải!

Thử nghiệm với một nơi bảng băm (CLISP):

> (macroexpand '(push-if new test (gethash a b))) 
(LET* 
((#:VALUES-12736 (MULTIPLE-VALUE-LIST (VALUES A B))) 
    (#:G12732 (POP #:VALUES-12736)) (#:G12733 (POP #:VALUES-12736))) 
(LET ((#:G12735 (GETHASH #:G12732 #:G12733))) 
    (WHEN (FUNCALL TEST #:G12735) (SETF #:G12734 (CONS NEW #:G12735)) 
    (SYSTEM::PUTHASH #:G12732 #:G12733 #:G12734)))) ; 

Aha; bây giờ có một số mã thú vị hơn được tạo ra để tránh đánh giá ab hai lần. Hàm gethash được gọi một lần, nhưng đối số của nó là các biến gensym. Giá trị cũ được chụp là #:G12735. Các thử nghiệm được áp dụng cho nó, và nếu nó vượt qua, các cửa hàng variabel #:G12734 được cập nhật với một giá trị danh sách cũ với new consed ở phía trước của nó. Sau đó, giá trị đó được đưa vào bảng băm với system::puthash.

Vì vậy, trong triển khai Lisp này, không có cách nào để tránh hai thao tác bảng băm để thực hiện cập nhật: gethashsystem::puthash. Đây là điều tốt nhất chúng ta có thể làm và hy vọng rằng hai người làm việc như một cặp được tối ưu hóa.

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