Idea for avoiding GC for lambdas

Steven Schveighoffer schveiguy at gmail.com
Mon Jun 21 20:01:27 UTC 2021


I have some code in my codebase that does stuff like this:

```d
struct S
{
    int id;
    int field1;
}

auto foo(S[] arr, int someval) // @nogc not allowed :(
{
    // remove all the elements of arr that have someval for field1
    import std.algorithm;
    return arr.filter!(v => v.field1 == someval);
}
```

While this works, and is nice, it's using a lambda with local references 
to filter data. This means a GC closure allocation is involved.

However, the value that is referenced (someval) is the only thing 
needed, and it seems a huge waste to allocate a stack frame *just* for that.

Let's look at another way to do this. `filter` does not have an overload 
which accepts a *value* to compare to, but `find` does. So let's build a 
new `filter` that uses it:

```d
auto nogcfilter(alias pred, T, V)(T[] rng, V val)
{
     import std.range;
     import std.algorithm : find;
     static struct Filter {
         T[] src;
         V val;
         auto front() { return src.front; }
         void popFront() {src.popFront; src = src.find!pred(val); }
         auto empty() { return src.empty; }
     }
     return Filter(rng, val);
}

auto foo(S[] arr, int someval) @nogc // :)
{
    // note: the explicit lambda types are needed for some reason
    return arr.nogcfilter!((S v, int a) => v.field1 == a)(someval);
}
```

Now we get filter capability, but without the GC.

This works great because the context items used do NOT require any 
mutability. It also works great because the filter function is building 
a custom type ANYWAY.

Now, we could add this kind of functionality to `filter`, but being the 
lazy programmer that I am, what if the compiler could do this for me? In 
most cases, I don't want to sleuth out what context is needed (which 
might change as the code evolves). To force the user to both invent the 
context type (here, it's just an `int`, but it may require other values) 
and explicitly pass it is a bit verbose (I already don't like having to 
pass the `int`, and declaring the parameter).

What if, a function that accepts a lambda, could also accept a *context* 
which the compiler made up and passed in? Here's how it would possibly 
look (with a new magic type `__context`):

```d
auto nogcfilter(alias pred, T, __context...)(T[] rng, __context ctxt)
{
     import std.range;
     import std.algorithm : find;
     static struct Filter {
         T[] src;
         __context val;
         auto front() { return src.front; }
         void popFront() {src.popFront; src = src.find!pred(val); }
         auto empty() { return src.empty; }
     }
     return Filter(rng, ctxt);
}

auto foo(S[] arr, int someval) @nogc
{
    return arr.nogcfilter!((v) => v.field1 == someval);
}
```

The compiler would see there's a lambda involved, automatically pass the 
context parameter `someval` into the function itself, and everything 
magically works. It would only be used when a closure would otherwise be 
allocated, and the function being called declares the context parameter.

Disclaimer: I am not a language developer.

Does this make sense? Is there anything feasible that might be similar? 
Would it be more feasible with different syntax?

I realize there are implications if the context parameter is mutable, so 
perhaps we would have to require only const contexts (though the 
compiler might be able to figure out that something isn't modified 
anywhere, and allow it to go in).

-Steve


More information about the Digitalmars-d mailing list