[gambit-list] Re --enable-auto-forcing: Scope, how/where implemented, how build (with) it properly?

Marc Feeley feeley at iro.umontreal.ca
Mon Sep 18 11:55:38 EDT 2017


> On Sep 8, 2017, at 7:30 AM, Adam <adam.mlmb at gmail.com> wrote:
> 
> Dear Marc,
> 
> Thank you very much for your clarification. Followup question below, to really understand what you are saying.
> 
> 2017-08-15 21:53 GMT+08:00 Marc Feeley <feeley at iro.umontreal.ca>:
> 
> > On Jul 27, 2017, at 6:45 AM, Adam <adam.mlmb at gmail.com> wrote:
> >
> > I'll make closer benchmarking of auto forcing later, but, it incurs a quite steep overhead.
> 
> Experimental data please!
> 
> I will provide it.
>   
> > Meanwhile, I have a question:
> >
> >
> > With --enable-auto-forcing, at procedure calls, that is (procedure 'arg1 'arg2 'etc.), |procedure| is forced, as we can see by the example that in both interpreted and compiled mode, ((delay (begin (println "Forced.") (lambda (v) v))) #t) evaluates (and it prints out "Forced.").
> >
> >
> > I'd wildly guess that quite a lot of the overhead in auto-forcing is here. If you're sure the operator will never be a promise in any procedure call, then this particular aspect of auto forcing could be disabled.
> >
> > (Without checking, I would guess that while Gambit may optimize so that local procedure calls may avoid being brought through the |force| logics, then, this optimization would do so that calls to procedures defined in *other* modules such as the runtime, would *not* be taken through |force| also. That should be positive for speed.)
> >
> >
> > Would it be possible for me to disable this particular aspect of the auto-forcing, to get higher performance?
> 
> Currently auto-forcing only works in interpreted code.  So if your program does (f x) and (car y) and you compile that, then “f” will not be forced and “y” will not be forced if car is inlined, for example if you (declare (not safe)).  You can consider this a bug… to be fixed.

Actually this is not quite true… f will be forced in the call (f x) thanks to this definition in lib/_kernel.scm:

(define-prim (##apply-with-procedure-check oper args)
  (##declare (not interrupts-enabled))
  (macro-force-vars (oper)
    (if (##procedure? oper)
      (##apply oper args)
      (##raise-nonprocedure-operator-exception oper args #f #f))))

> 
> However, compiled predefined functions do the auto-forcing (when the system is configured with --enable-auto-forcing).  So (car y) will force y if the actual function is called, for example if you (declare (safe)), which is the default.
> 
> Note that for “safe” compiled code, when compiling (f x) where f is a mutable global variable, it is necessary to generate a check that f is a procedure.  If it is not a procedure a handler is called that normally raises an exception.  This handler could easily be extended to first force the value and check if the resulting value is a procedure and proceed with the call if it is (hooray for raising exceptions in tail-position!).  So there would be no additional cost for safe compiled code.
> 
> Can you please clarify what you mean here, by giving one or two pieces of example code, that illustrate the difference between various code operation options - so.. that would be
> 
> 1) Interpreted code, vs.
> 2) Compiled code with (declare (safe)), vs.
> 3) Compiled code with (declare (not safe))
> 
> (I guess maybe (declare (block)) vs. (declare (separate)) could affect forcing behavior, as inlined code not would be forced, but non-inlined code could.)
> 

Take this code, an explicit definition of a simple map function:

(define (mymap f lst)
  (if (pair? lst)
      (let ((elem (car lst)))
        (cons (f elem)
              (mymap f (cdr lst))))
      '()))

(pp (mymap (delay (lambda (x) (+ x 1))) '(10 20 30)))

Note that the first parameter to mymap is a promise whose forced value is a function.

In a system built with --enable-auto-forcing, the code runs fine in the interpreter and when compiled.

However, when (declare (not safe)) is used the primitive operations (car, cdr, …) will not automatically force the arguments, and the call operation will not automatically force the “operator” position, i.e. the function being called.  So this code doesn’t work as expected when compiled:

(define (mymap f lst)
  (if (pair? lst)
      (let ((elem (car lst)))
        (cons (let () (declare (not safe)) (f elem))
              (mymap f (cdr lst))))
      '()))

(pp (mymap (delay (lambda (x) (+ x 1))) '(10 20 30)))

The compiler converts (car lst) into (##car lst) instead of the correct (##car (##force lst)).  Also, when using the default (declare (safe)) the compiler converts (car lst) into

    (if (##pair? lst) (##car lst) (car lst))

rather than the correct

    (let ((lst (##force lst)))
      (if (##pair? lst) (##car lst) (car lst)))

> 
> 
> In either case if I understand you right, there are examples where Gambit with auto-forcing enabled, will fail executing ((delay (lambda () 'hello-world))) .

With --enable-auto-forcing, this case works fine with (declare (safe)), but will not work fine with (declare (not safe)).

> 
> Also.. if I understand you right, there are cases when Gambit with auto-forcing enabled also would fail evaluating (abs (delay 0)) .

With --enable-auto-forcing, this case works only when abs is not inlined, in other words an actual function call to abs is performed (because the library definition correctly forces the argument).  This can be achieved in various ways, like (declare (not run-time-bindings abs)) or (declare (standard-bindings) (not inline-primitives abs)).

This needs to be fixed so that those declarations, which typically improve execution speed, can be used reliably in a system built with --enable-auto-forcing.

> 
> Only for my clarity, please tell in what interval of conditions these will fail.
> 
> 
> Thanks!

Marc




More information about the Gambit-list mailing list