lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]


So continuing on:
Le mer. 22 juil. 2020 à 06:28, Philippe Verdy <verdyp@gmail.com> a écrit :
   meta = {
      __newindex = function(t, k, v)
         print('new t['..tostring(k)..']='..tostring(v))
         t|k] = v
      end,
   }
   t = meta::{[ident(1)] = ident(false), [ident(2)] = ident(true), [ident(3)] = ident('other'),}

there's an "equivalent" code for the second statement:
    t = table.new(3); setmetatable(t, meta)
    t[ident(1)] = ident(false); t[ident(2)] = ident(true); t[ident(3)] = ident('other');

It is using table.new(int) to specify an hint for the initial maximum size and avoid the resizing overhead: this size is predetermined from the total number of key values indicated syntaxically, even if these keys may be duplicate, or if some values may be nil.

The metatable is then set first, then for each key/value pair, the key is evaluated, then the value, then the assignment using __newindex(t,k,v) instead of rawset(t,k,v) (because we specified a metatable and that table defines a "__newindex").

So all is interleaved, and in a welldefined order: it is warrantied that the last value indicated in the table constructor will be in the last key indicated in the constructor (or that it will be nil), but there's no such warranty for previous keys that may be overwritten, or deleted by further assignment.

Some of the values evaluated and assigned to some key may have be replaced and will be garbage collected, but the table will still maintain its size, not necessarily the same as #t or table.maxn(t), as the constructed table is still not limited to pure sequences.

This order is fully consistant with the use of table for implementing inheritance. But a JIT compiler trying to optimize this may attempt to create a fixed size array for keys and values. However the assignment of keys and values in the temporary tables should not affect at all the construction of the table: these two arrays will not be prefilled (except if their values are static constants not requiring any further evaluation, so the two arrays may be a simple "virtual" copy of static unmutable arrays

Then the JIT compiler should still evaluate the array values of members in order: either from the constant unmutable array prebuilt by the compiler, or by compiling the code to evaluate the key and the value to pass to __newindex (or rawset).
The compiler will not need to evaluate the table reference (t) multiple times, it managed this reference internally before returning the fully built table whose metatable was set early just after initializing it with its default size.

This way, the GC impact is minimal (some GC operations may occur after the construction (rarely in the middle), only after using __newindex(t,k,v) with a duplicate key in k (overwriting an existing non-nil value set by a previous key/pair assignment); such GC may occur in the middle because the overwritten value is no longer referenced and memory may be requested during the evaluation of further keys or values specified later inside the syntax of the constructor.

My proposal is then consistant and should define the expected behavior while allowing a JIT to perform fast and generate an efficient code for the constructor.

It will be compatible as well with existing runtime engines (the syntax I propose just makes things a bit simpler: no need to declare a local variable for the reference to the new table, no need to use setmetatable(), no need to make so many assignments repeating the reference to the table in construction, so it works as well when a table constructor is used in the middle of and _expression_ (where it is not possible syntaxically to insert multiple statements, even if this is not a problem at all for the runtime VM: this is only a current syntaxic limitation).

My proposed syntax "meta::{{ [key]=value, ... }" is simple to read as well. Its runtime behavior is well defined (fully specified order of execution, fully predictable result for the resulting table, including values of keys, and table maximum size), single use of the allocator (if keys form a sequence without large "holes"), and minimal GC (no GC impact at all if all keys are distinct). For initializing tables that are pure sequences of distinct integer keys (even if some values are nil), there will be only ONE allocation.

The JIT may optimize the initial table allocation by checking the value of the first evaluated key, then using it to determine the size to allocate as this key value plus the number of additional keys still not evaluated: it may allocate the table only after the evaluation of this first key and the first value: if that value is not nil, it may assume this will be the minimal key value, if that first value is nil, the allocation of the new table is still suspended until it finds a non-nil value to assign: the first non-nil value returned determined the estimated lowest key that will be used. If the table contains a static key [1] (at any position), no suspension is needed, the initial table size is predetermined by the total number of key/value pairs.

The same is true if the table contain any static key [n] where n is an integer lower than the total number of keys in the constructor.
Large static keys like [1000] may not be used at all for the initial size to use for calling table.new(sz), if the constructor syntaxically contains less than 1000 keys/values pairs, but for this case, the constructor may use the total number of keys multiplied by some reasonnable factor (e.g. 1.25) then rounded as long as it remains lower than this static [1000] key, so for a table constructor like:
  t = {[1000] = 1000, [100]=100},
it would allocate a new table of size round(2* 1.25) = round(2.5) = 3...

And no longer need to make any reference to the core library table.new(sz) and setmetatable(t,k,v) which may have been obscured in the environment.