From: Dave Bakhash
Subject: basic question (probably w/ no good answer)
Date: 
Message-ID: <m3d7o1cqaw.fsf@lost-in-space.ne.mediaone.net>
Hi,

suppose I have a function that looks like this:

(defun f (x y)
 (declare (type blah x y)
	  (special *other*))
 (let ((this (+ x y))
       (that (- x y)))
   (now-do-some-stuff-using this that)
  ) ;; #### should I end the LET here or not?
 (compute-and-return-stuff-thats-indep-of-this-and-that *other*))

the question is whether or not it's better to keep final Lisp code in
a function inside the body of an outer LET or to close the LET and add
another expression that's at the same level as LET.  

Of course I'm imagining that the compiler will (should) DTRT either
way, but I just want to know which is easier for most compilers
to figure out the more eficient way?

My guess: don't close off the LET, but keep things nested, even
though those final expressions don't need to be inside the LET.

dave

From: Erik Naggum
Subject: Re: basic question (probably w/ no good answer)
Date: 
Message-ID: <3164150875655283@naggum.no>
* Dave Bakhash <·····@alum.mit.edu>
| My guess: don't close off the LET, but keep things nested, even
| though those final expressions don't need to be inside the LET.

  my rule of thumb: unless you use a binding form for its progn value,
  i.e., in situations where you may use only one form, move the subforms
  that don't need the bindings out of the binding form.  this makes it
  clear when the scope of the bindings cease to exist, reducing the amount
  of work a reader of the code would have to go through to figure out that
  the bindings _don't_ affect these forms, and it makes it clear that the
  binding form does not return a useful value, but is there for side
  effect.  clues like this can make reading code a lot easier.

  this is likely a matter of some personal taste.

#:Erik
From: Rob Warnock
Subject: Re: basic question (probably w/ no good answer)
Date: 
Message-ID: <8cmckp$17gqp$1@fido.engr.sgi.com>
Erik Naggum  <····@naggum.no> wrote:
+---------------
| * Dave Bakhash <·····@alum.mit.edu>
| | My guess: don't close off the LET, but keep things nested, even
| | though those final expressions don't need to be inside the LET.
| 
|   my rule of thumb: unless you use a binding form for its progn value,
|   i.e., in situations where you may use only one form, move the subforms
|   that don't need the bindings out of the binding form.  this makes it
|   clear when the scope of the bindings cease to exist, reducing the amount
|   of work a reader of the code would have to go through to figure out that
|   the bindings _don't_ affect these forms...
+---------------

And also allows the garbage collection of the values of the bindings sooner...


-Rob

-----
Rob Warnock, 41L-955		····@sgi.com
Applied Networking		http://reality.sgi.com/rpw3/
Silicon Graphics, Inc.		Phone: 650-933-1673
1600 Amphitheatre Pkwy.		PP-ASEL-IA
Mountain View, CA  94043
From: Joe Marshall
Subject: Re: basic question (probably w/ no good answer)
Date: 
Message-ID: <u8zyo1r4j.fsf@alum.mit.edu>
Dave Bakhash <·····@alum.mit.edu> writes:

> Hi,
> 
> suppose I have a function that looks like this:
> 
> (defun f (x y)
>  (declare (type blah x y)
> 	  (special *other*))
>  (let ((this (+ x y))
>        (that (- x y)))
>    (now-do-some-stuff-using this that)
>   ) ;; #### should I end the LET here or not?
>  (compute-and-return-stuff-thats-indep-of-this-and-that *other*))
> 
> the question is whether or not it's better to keep final Lisp code in
> a function inside the body of an outer LET or to close the LET and add
> another expression that's at the same level as LET.  
> 
> Of course I'm imagining that the compiler will (should) DTRT either
> way, but I just want to know which is easier for most compilers
> to figure out the more efficient way?

Why do you care about making things easier for the compiler?  Even if
the compiler generated different code for these two options, why would 
you care?  

The time you might `save' in compilation is negligable, a decent
compiler would produce similar code in either case, even if the code
were different, I can't imagine it would be so different as to have a
noticable impact on performance or code size, and if it *really* made
a noticable difference, I'd imagine that the compiler would be so
piss-poor as to be virtually unusable.

> My guess: don't close off the LET, but keep things nested, even
> though those final expressions don't need to be inside the LET.

Making a hard-and-fast rule about this would lead to ridiculous coding 
style if taken to the limit.  I was looking at a bunch of code to try
to find some examples to see how I do it, but I found out that the
only place where this becomes an issue is when you are writing code
that makes use of side effects (consider that the value of
`now-do-some-stuff-using' is discarded).

If the final expressions are `conceptually' part of the let, then keep 
them inside, otherwise put them outside.
From: Erik Naggum
Subject: Re: basic question (probably w/ no good answer)
Date: 
Message-ID: <3164210231625803@naggum.no>
* Joe Marshall <·········@alum.mit.edu>
| The time you might `save' in compilation is negligable, a decent compiler
| would produce similar code in either case, even if the code were
| different, I can't imagine it would be so different as to have a
| noticable impact on performance or code size, and if it *really* made a
| noticable difference, I'd imagine that the compiler would be so piss-poor
| as to be virtually unusable.

  I think "helping the compiler" (not expressed in so many words, but in
  meaning) is a metaphor that relates to the "sufficiently smart compiler",
  and that it is not directly related to any actual assistance.  it's more
  like "would it hamper the smart compiler's ability to be as smart as it
  could be", or, as David put it "do the right thing".  I don't think so,
  but it's a good question, nonetheless.  Rob pointed out that releasing
  the hold on the objects might help (again, metaphorically) the garbage
  collector pick up dead objects earlier, although it is hard for me right
  now to imagine how this could b affected: scope analysis would cause the
  bindings to go out of scope either way, and if these variables were in
  call-frame slots or registers, they wouldn't necessarily be any more
  garbage unless these slots were explicitly set to nil or something
  similarly drastic (because the savings would be so limited compared to
  the cost of doing this all over the place).

| Making a hard-and-fast rule about this would lead to ridiculous coding
| style if taken to the limit.

  I'll trust readers of hard-and-fast rules to exercise their judgment and
  not take things to ridiculous limits.

  I think the question merits thought -- it's one of those small things
  that it's too easy to waste time on because we're _not_ thinking about
  it, and I for one think this question and questions like it would make it
  a lot easier for more experienced programmers to help new programmers get
  a feel for the language in actual use.  this is the "experience" part
  that you get from reading lots of code and spending your working hours
  among other Lisp programmers.  I think it's nice to see this newsgroup
  assume part of that role, too.

#:Erik
From: Rob Warnock
Subject: Re: basic question (probably w/ no good answer)
Date: 
Message-ID: <8cojod$1gq6f$1@fido.engr.sgi.com>
Erik Naggum  <····@naggum.no> wrote:
+---------------
| Rob pointed out that releasing the hold on the objects might help
| (again, metaphorically) the garbage collector pick up dead objects
| earlier, although it is hard for me right now to imagine how this
| could be affected: scope analysis would cause the bindings to go out
| of scope either way, and if these variables were in call-frame slots
| or registers, they wouldn't necessarily be any more garbage unless
| these slots were explicitly set to nil or something similarly drastic
| (because the savings would be so limited compared to the cost of doing
| this all over the place).
+---------------

I was thinking more low-tech, I guess, of (metaphorically) helping the
garbage collector get rid of large intermediate temporaries as soon as
possible, especially if you "knew" you were about to do some more heavy
allocating immediately after the LET block. Quickly-contrived example
[sorry, I don't know the standard CL function for "tree-flatten" a.k.a.
"fringe"]:

	(defun foo (big-data-structure1 big-data-structure2)
	  ...
	  (let (tmp3)
	    (let* ((tmp1 (map-tree #'some-func big-data-structure1))
	           (tmp2 (map-tree #'some-func big-data-structure2)))
	      (setq tmp3 (reduce #'+ (mapcar #'some-scoring-func
					     (tree-flatten tmp1)
					     (tree-flatten tmp2)))))
	    ;; At this point, tmp1 & tmp2 are garbage, and any GC triggered
	    ;; by the following allocation will be able to use the reclaimed
	    ;; space and perhaps not have to expand memory.
	    (memory-grabbing-func tmp3)))

Yes, I know that re-writing the whole thing in completely functional style
would do exactly the same thing:

	(defun foo (big-data-structure1 big-data-structure2)
	  (memory-grabbing-fun
	    (reduce
	      #'+
	      (mapcar
		#'some-scoring-func
		(tree-flatten (map-tree #'some-func big-data-structure1))
		(tree-flatten (map-tree #'some-func big-data-structure2))))))

but when you're writing/debugging incrementally, you sometimes want those
intermediates available for inspection/printing, and then somehow you never
seem to get around to rewriting into functional purity...


-Rob

-----
Rob Warnock, 41L-955		····@sgi.com
Applied Networking		http://reality.sgi.com/rpw3/
Silicon Graphics, Inc.		Phone: 650-933-1673
1600 Amphitheatre Pkwy.		PP-ASEL-IA
Mountain View, CA  94043
From: Robert Monfera
Subject: Re: basic question (probably w/ no good answer)
Date: 
Message-ID: <38F0253E.1F4488FC@fisec.com>
Rob Warnock wrote:

> I was thinking more low-tech, I guess, of (metaphorically) helping the
> garbage collector get rid of large intermediate temporaries as soon as
> possible, especially if you "knew" you were about to do some more heavy
> allocating immediately after the LET block. Quickly-contrived example
> [sorry, I don't know the standard CL function for "tree-flatten" a.k.a.
> "fringe"]:
>
>         (defun foo (big-data-structure1 big-data-structure2)
>           ...
>           (let (tmp3)
>             (let* ((tmp1 (map-tree #'some-func big-data-structure1))
>                    (tmp2 (map-tree #'some-func big-data-structure2)))
>               (setq tmp3 (reduce #'+ (mapcar #'some-scoring-func
>                                              (tree-flatten tmp1)
>                                              (tree-flatten tmp2)))))
>             ;; At this point, tmp1 & tmp2 are garbage, and any GC
>             ;; by the following allocation will be able to use the
>             ;; space and perhaps not have to expand memory.
>             (memory-grabbing-func tmp3)))

The compiler performs a data flow analysis, and the function will forget
about tmp1 and tmp2 right after flattening even if you use one single
LET* with all three bindings.  There was a recent thread about why the
debugger does not see things inside a LET scope at sufficiently high
speed and low debug optimization.  Maybe even the same register will be
allocated to tmp3 as to tmp1 as a result of register coloring.  Also,
maybe all three bindings will be stack-allocated.  The programmer or the
compiler should not even consider how much consing
#'memory-grabbing-func is expected to do for scoping decisions.

> Yes, I know that re-writing the whole thing in completely functional style
> would do exactly the same thing:
>
>         (defun foo (big-data-structure1 big-data-structure2)
>           (memory-grabbing-fun
>             (reduce
>               #'+
>               (mapcar
>                 #'some-scoring-func
>                 (tree-flatten (map-tree #'some-func big-data-structure1))
>                 (tree-flatten (map-tree #'some-func big-data-structure2))
>                 ))))

I expect a good compiler to generate identical machine code at (debug 0)
with both pieces of code, as well as with a version containing

(let* ((tmp1 ...)
       (tmp2 ...)
       (tmp3 ...))
   ...)

As far as readability (for humans) is concerned, using two LETs and a
SETQ instead of one single LET* seems to do more harm than good, SETQ
being rather ungainly for such purposes.  If a function is so complex
that it really needs such meticulous scoping to help human readers, this
is a cleaner way to say that the sole purpose of tmp1 and tmp2 is to
help calculate tmp3:

(defun foo (big-data-structure1 big-data-structure2)
  (let ((bar
         (let ((baz (map-tree #'some-func big-data-structure1))
               (zik (map-tree #'some-func big-data-structure2)))
           (reduce #'+ (mapcar #'some-scoring-func
                         (tree-flatten baz)
                         (tree-flatten zik))))))
    (memory-grabbing-func bar)))

> but when you're writing/debugging incrementally, you sometimes want those
> intermediates available for inspection/printing, and then somehow you
> never seem to get around to rewriting into functional purity...

You could inspect and print the results of map-trees directly too, and
all versions are at the same level of functional purity.  The benefit of
LET for humans is that bindings have explanatory names serving as
documentation (at the only expense of visual verbosity).  For this
reason, naming a binding tmp is pointless (except if it is something
recognizable like (* 4 a c), but even then it would deserve a name).

You are right with the avoidance of rewriting.  Using LET never hurts
performance, and improves readability if well placed.  But it's not just
that there is no point in rewriting such expressions - you would lose
documentation and have to re-test the function.

Nit-picking: Both flattening and mapping should be inside the
calculation for tmp1 and tmp2, otherwise the optimizing that you were
seeking with scoping would not be achieved even if explicit scoping did
help the compiler.  Also, the trees can be flattened before mapping, but
I guess the calculation was only an example.

(defun foo (big-data-structure1 big-data-structure2)
  (let ((bar
         (let ((baz (mapcar #'some-func (flatten big-data-structure1)))
               (zik (mapcar #'some-func (flatten big-data-structure2))))
           (reduce #'+ (mapcar #'some-scoring-func baz zik)))))
    (memory-grabbing-func bar))))

Robert
From: David Bakhash
Subject: Re: basic question (probably w/ no good answer)
Date: 
Message-ID: <m3k8i2uy3q.fsf@alum.mit.edu>
I wasn't trying to make a hard-and-fast rule.  I wasn't trying to
over-simplify something that might be subtle.  I only asked because it 
seemed to me that there are opportunities in writing Lisp to go either 
way sometimes, and having very limited compiler-level understanding of 
CL, I was just seeking guidance.

Anyway, I'm happy I asked, because my "style" has now changed, and I
know a little more, and have a better idea about how other people look 
at the situation.

dave
From: Tim Bradshaw
Subject: Re: basic question (probably w/ no good answer)
Date: 
Message-ID: <ey31z4gs4pi.fsf@cley.com>
* Dave Bakhash wrote:

> the question is whether or not it's better to keep final Lisp code in
> a function inside the body of an outer LET or to close the LET and add
> another expression that's at the same level as LET.  

> Of course I'm imagining that the compiler will (should) DTRT either
> way, but I just want to know which is easier for most compilers
> to figure out the more eficient way?

I think any reasonable compiler will spot the bindings are dead
anyway.

> My guess: don't close off the LET, but keep things nested, even
> though those final expressions don't need to be inside the LET.

I would tend to do the opposite, because I tend to prefer code where
bindings have the smallest scope possible, but this is from the point
of view of readability (by me) & avoiding bugs (the fewer names that
are legal at any given point, the harder it is for me to mistype one
for another).  I also wouldn't claim this as a general rule.

--tim