Оптимизация "float to pointer coercion" боксинга

Jan 15, 2010 14:46

Заметка из серии "perfomance hints".

Допустим, у нас есть какая-то функция от двух double-float, которая должна работать максимально быстро:

(defun fast-proc (x y)
(declare (optimize (speed 3) (safety 0) (debug 0) (space 0)))
(declare (type double-float x)
(type double-float y))
(> (+ x y) 10.0d0))
В нашем случае, она просто проверяет, действительно ли сумма двух чисел больше десяти. Ассемблер получается вполне вменяемым:

; disassembly for FAST-PROC
; 045A066D: F20F58D1 ADDSD XMM2, XMM1 ; no-arg-parsing entry point
; 71: 660F2F155F000000 COMISD XMM2, [RIP+95]
; 79: BA4F001020 MOV EDX, 537919567
; 7E: 41BB17001020 MOV R11D, 537919511
; 84: 490F4AD3 CMOVP RDX, R11
; 88: 490F46D3 CMOVBE RDX, R11
; 8C: 488BE5 MOV RSP, RBP
; 8F: F8 CLC
; 90: 5D POP RBP
; 91: C3 RET
/skip/

Вроде все хорошо, но при использовании мы все равно упираемся в засаду. Допустим, так:

(defun call-fast-proc (x y)
(declare (optimize (speed 3) (safety 0) (debug 0) (space 0)))
(declare (type double-float x)
(type double-float y))
(let ((new-x (+ x 1.0d0))
(new-y (+ y 1.0d0)))
(fast-proc new-x new-y)))

Получаем кривой ассемблер и ругань:

; in: DEFUN CALL-FAST-PROC
; (CL-USER::FAST-PROC CL-USER::NEW-X CL-USER::NEW-Y)
;
; note: doing float to pointer coercion (cost 13)
;
; note: doing float to pointer coercion (cost 13)

Это означает, что sbcl не может сообразить, как упаковать double-float и вынужден его забоксить. И "cost 13" в этом как-бы полбеды, самая засада в том, что здесь начинает требоваться GC для аллокации этого "pointer", что на длинных циклах приводит к весьма противному оверхеду.

Как корректно выйти из этой ситуации без инлайна я не смог разобраться, но выкрутился таким образом:

(defstruct fast-proc-ctx
(x 0.0d0 :type double-float)
(y 0.0d0 :type double-float)
(proc nil :type (or null (function () boolean))))

(defmacro fast-proc (ctx x y)
(with-gensyms (proc)
`(let ((,proc (fast-proc-ctx-proc ,ctx)))
(declare (type (function () boolean) ,proc))
(setf (fast-proc-ctx-x ,ctx) ,x)
(setf (fast-proc-ctx-y ,ctx) ,y)
(funcall ,proc))))

(defun make-fast-proc ()
(let ((ctx (make-fast-proc-ctx)))
(setf (fast-proc-ctx-proc ctx)
(lambda ()
(declare (optimize (speed 3) (safety 0) (debug 0) (space 0)))
(let ((x (fast-proc-ctx-x ctx))
(y (fast-proc-ctx-y ctx)))
(declare (type double-float x)
(type double-float y))
(> (+ x y) 10.0d0))))
ctx))

(defun call-fast-proc (x y)
(declare (optimize (speed 3) (safety 0) (debug 0) (space 0)))
(declare (type double-float x)
(type double-float y))
(let* ((ctx (make-fast-proc))
(new-x (+ x 1.0d0))
(new-y (+ y 1.0d0)))
(fast-proc ctx new-x new-y)))

Основная идея здесь: передавать параметры не напрямую, а через контекст. В саму же функцию не передается вообще ничего, она этот контекст видит из замыкания.

В данном варианте замыкание может и не нужно (можно создать глобальный объект или передавать контекст напрямую), но в реальной моей программе это оказалось удобней. Ну да не важно, главное, я донес идею, как избавится от боксинга :) Кстати, через этот же контекст можно и результат возвращать, помогает от жалоб: "note: doing float to pointer coercion (cost 13) to "".

code, perfomance, optimization, lisp, common lisp, hint

Previous post Next post
Up