Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

basic question (probably w/ no good answer)

2 views
Skip to first unread message

Dave Bakhash

unread,
Apr 8, 2000, 3:00:00 AM4/8/00
to
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

Erik Naggum

unread,
Apr 8, 2000, 3:00:00 AM4/8/00
to
* Dave Bakhash <ca...@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

Rob Warnock

unread,
Apr 8, 2000, 3:00:00 AM4/8/00
to
Erik Naggum <er...@naggum.no> wrote:
+---------------

| * Dave Bakhash <ca...@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 rp...@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

Joe Marshall

unread,
Apr 8, 2000, 3:00:00 AM4/8/00
to
Dave Bakhash <ca...@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.

Erik Naggum

unread,
Apr 8, 2000, 3:00:00 AM4/8/00
to
* Joe Marshall <jmar...@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

Tim Bradshaw

unread,
Apr 8, 2000, 3:00:00 AM4/8/00
to
* 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

Rob Warnock

unread,
Apr 9, 2000, 3:00:00 AM4/9/00
to
Erik Naggum <er...@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...

Robert Monfera

unread,
Apr 9, 2000, 3:00:00 AM4/9/00
to

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

David Bakhash

unread,
Apr 13, 2000, 3:00:00 AM4/13/00
to
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

0 new messages