You say
<quote>
- The source object returns multiple values as a tuple (for good reasons).
- The block processes theses values but needs another argument (at the
first place).
</quote>

On the first point, we have to take your word for it.
It's not clear why you could not pass an n+1-element array to
the source method and have it fill in elements after the first
rather than having it allocate a new array.  If you are concerned
about object allocation in a tight loop this would be a good
place to start.

argArray := Array new: block argumentCount.
...
   source compute: i into: argArray.
   argArray at: 1 put: i.
   block valueWithArguments: argArray.

If for some reason it is utterly impossible to modify
'source compute: i' in this way, we can *still* use the
technique of allocating an array once for the whole loop.

argArray := Array new: block argumentCount.
...
   argArray at: 1 put: i;
     replaceFrom: 2 to: argArray size with: (source compute: i).
   block valueWithArguments: argArray.

This seems like the smallest possible change to your code.
You still have the overhead of copying from one array to another
-- which is why I prefer modifying #compute: -- but you do not
have the overhead of allocating an array per iteration.

On the second point, right there you have the assumption that is
limiting your vision.

You are viewing the problem as "pass an extra first argument to
the block" when you *should* frame it as "ensure that the block
knows the value of i SOMEHOW".  Presumably these blocks are
generated by code written by you.

So let's start with
Someclass
  methods for: 'generating blocks'
    blockFor: aSituation
      ^[:x0 :x1 ... :xn | ....]

    block := Someclass blockFor: theSourceSituation.
    (1 to: 1000) do: [:i | | args |
      args := source compute: i.
      block valueWithArguments: {i} , args

So now we change it to

Someclass
  methods for: 'generating blocks'
    blockFor: aSituation sharing: stateObject
      ^[:x1 ... :xn | |x0|
          x0 := stateObject contents.
          .....]

    ref := Ref with: 0.
    block := Someclass blockFor: theSourceSituation sharing: ref.
    1 to: 1000 do: [:i | |args|
      ref contents: i.
      args := source compute: i.
      block valueWithArguments: args].

Ref is an actual class in my library modelled on the Pop-2 and SML
types of the same name.  It's not important.  What *is* important is
that information can be supplied to a block through a shared object
as well as through a parameter.

I am a little bit twitchy about the 'coincidence' of the size of
#compute:'s result and the argument count of the block.  Why not
pass the block to #compute: so that there never is any array in
the first place?

compute: index
   ... ^{e1. ... en} ...

=>
compute: index thenDo: aBlock
   ... ^aBlock value: e1 ... value: en ...

    ref := Ref with: 0.
    block := Someclass blockFor: theSourceSituation sharing: ref.
    1 to: 1000 do: [:i | |args|
      ref contents: i.
      source compute: i thenDo: block].

Now there are even fewer arrays being allocated and no use of
#valueWithArguments: in any guise.

The problem is NOT, as some commentators apparently think, that
you are using a block.  The problem is that having thought of
one way to wire things up -- not an unreasonable way, in fact --
you concentrated on making *that* way faster instead of looking
for other ways to do it.

We have several idioms here:
  Reuse Object (convert an allocation per iteration to an allocation
  per loop by reinitialising a object instead of allocating a new one)
  Communicate Through Shared Microstate (communicate information between
  a method and a block or object through a 'microstate' object created
  by the method and passed when the block or object is created)
  Multiple Values by Callback (instead of 'returning' multiple values in
  a data structure, pass a block to receive those values as parameters).



On Wed, 12 Apr 2023 at 04:44, Steffen Märcker <merk...@web.de> wrote:

> Hi!
>
> First, thanks for your engaging answers Richard, Stephane and the others!
>
> The objective is to avoid unnecessary object creation in a tight loop that
> interfaces between a value source and a block that processes the values.
> - The source object returns multiple values as a tuple (for good reasons).
> - The block processes theses values but needs another argument (at the
> first place).
> We do not know the number of values at compile time but know that they
> match the arity of the block. Something like this (though more involved in
> practice):
>
>         (1 to: 1000) do: [:i | | args |
>                 args := source compute: i.
>                 block valueWithArguments: {i} , args ]
>
> Since prepending the tuple with the first argument and then sending
> #valueWithArguments: creates an intermediate Array, I wonder whether we can
> avoid (some of) that overhead in the loop without changing this structure.
> Note, "{i}, args" is only for illustration and creates an additional third
> array as Steve already pointed out.
>
> To sum up the discussion so far:
> - If possible, change the structure, e.g., processing the tuple directly.
> - Fast primitives exist for the special cases of 1, 2 and 3 arguments only.
> - Code for > 3 arguments would have to use #valueWithArguments: after all.
>
> Did I miss something?
>
> Kind regards,
> Steffen
>

Reply via email to