digitalmars.D - Idea for avoiding GC for lambdas
- Steven Schveighoffer (86/86) Jun 21 2021 I have some code in my codebase that does stuff like this:
- Steven Schveighoffer (4/5) Jun 21 2021 Obviously, that should have said *include* all the elements that have
- Elronnd (30/32) Jun 21 2021 The reason delegates involve GC is because the closure is shared.
- Steven Schveighoffer (26/57) Jun 22 2021 Right, I honestly don't know how the context is passed for an alias
- H. S. Teoh (27/37) Jun 22 2021 AFAIK, delegates are essentially just:
- Steven Schveighoffer (23/30) Jun 22 2021 This again, is a misunderstanding of my proposal. I'm not talking about
- H. S. Teoh (45/51) Jun 22 2021 [...]
- Steven Schveighoffer (11/21) Jun 23 2021 Yes, exactly! The only tricky part is that the filter function has to
- IGotD- (15/20) Jun 22 2021 The way C++ works is that a lambda (which is essentially a
- Ola Fosheim Grostad (4/12) Jun 21 2021 You are asking for C++ lambdas? No need for a new type of
- Steven Schveighoffer (6/21) Jun 22 2021 I was kinda hoping for the compiler to assist in the generation and
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (10/14) Jun 23 2021 Off the top of my head:
- Steven Schveighoffer (6/24) Jun 23 2021 Well, the library function needs to signify to the compiler that the
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (9/10) Jun 23 2021 Ok, so you use a templated filter function with a regular
- rm (4/25) Jun 21 2021 It does come up every so often. You can also follow the links here.
- Steven Schveighoffer (7/12) Jun 22 2021 Thanks for the link, I didn't remember this (it just happened a few
- 12345swordy (4/99) Jun 22 2021 Why can't it be implemented by using templates?
- Steven Schveighoffer (3/4) Jun 23 2021 Oh it can, I did in the original message! It just doesn't look as nice.
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
Jun 21 2021
On 6/21/21 4:01 PM, Steven Schveighoffer wrote:// remove all the elements of arr that have someval for field1Obviously, that should have said *include* all the elements that have someval. -Steve
Jun 21 2021
The reason delegates involve GC is because the closure is shared. E.G.: int x; auto f = () => x++; auto g = () => x++; f(); //0 g(); //1 f(); //2 f and g share a reference to the same x, so we need GC to know when to clean it up. Your proposal is essentially a value delegate; the __context stuff makes that explicit, but it's not actually fundamentally different from the current scheme, it's just that the context becomes a value type rather than a reference type. I think value closures are a good idea--for semantics reasons too, not just avoiding gc--but problematic for existing APIs to support, because every value closure will have a different type and a different size. And we have enough problems already with compile time template explosion :) WRT your proposal, I think it would be better to introduce an alternate syntax for function literals: we have function(parm) => ret and delegate(parm) => ret; add valuedelegate(parm) => ret. And make it so that valuedelegate.ptr is a struct (the context struct) so you can still introspect it if needed.I realize there are implications if the context parameter is mutable, so perhaps we would have to require only const contextsWhy? What you've made is basically a functor where the compiler infers the context; structs are received by reference (by their methods). If the context were shared, then const wouldn't be enough; it would have to be immutable to retain value semantics. But the whole point of this was to avoid sharing (and hence GC), so I don't see why that would be a problem.
Jun 21 2021
On 6/21/21 6:19 PM, Elronnd wrote:The reason delegates involve GC is because the closure is shared. E.G.: int x; auto f = () => x++; auto g = () => x++; f(); //0 g(); //1 f(); //2Yes, but only necessary if the data is mutated.Your proposal is essentially a value delegate; the __context stuff makes that explicit, but it's not actually fundamentally different from the current scheme, it's just that the context becomes a value type rather than a reference type.Right, I honestly don't know how the context is passed for an alias parameter. The compiler must pass some sort of context pointer to the calling function, which then gets stored somehow. But it's all hidden. If it's hidden, it might as well be as big as needed.I think value closures are a good idea--for semantics reasons too, not just avoiding gc--but problematic for existing APIs to support, because every value closure will have a different type and a different size.Remember, these aren't delegates in the strict sense, they are alias parameters. They can be implemented however D wants. Take a look at the `filter` [implementation](https://github.com/dlang/phobos/blob/61c00065a8c7efdf21efb92ce69d7a9de4904ec9/std/algorithm/itera ion.d#L1371-L1432), there's not a delegate in sight.And we have enough problems already with compile time template explosion :)This shouldn't have a significant effect on templates -- aliases are already unique parameters, causing a new template for every call.WRT your proposal, I think it would be better to introduce an alternate syntax for function literals: we have function(parm) => ret and delegate(parm) => ret; add valuedelegate(parm) => ret. And make it so that valuedelegate.ptr is a struct (the context struct) so you can still introspect it if needed.I'm unsure if this is how it works, the alias parameters aren't exactly delegates, though that might be how the compiler implements them. My thought was that the compiler could pick to send the function alias in as a straight alias (without context) and pass the context in as a separate parameter. The function itself would have a type that accepts the context data.I mean non-changeable contexts. These can be const data, as long as they aren't const references, they should be unchangeable. I.e. the compiler should be able to pass an x like: ```d const x = 5; ``` -SteveI realize there are implications if the context parameter is mutable, so perhaps we would have to require only const contextsWhy? What you've made is basically a functor where the compiler infers the context; structs are received by reference (by their methods). If the context were shared, then const wouldn't be enough; it would have to be immutable to retain value semantics. But the whole point of this was to avoid sharing (and hence GC), so I don't see why that would be a problem.
Jun 22 2021
On Tue, Jun 22, 2021 at 11:05:18AM -0400, Steven Schveighoffer via Digitalmars-d wrote:On 6/21/21 6:19 PM, Elronnd wrote:[...]AFAIK, delegates are essentially just: struct __dg { void* context; RetType function(void* context, Args...) func; } An alias parameter, being a template parameter, simply causes the aliased function to get template-expanded in the body of the delegate. Any context it carries must be shared with the delegate's context, which is the source of the recent issue with multi-context template functions that resulted in the compiler change getting reverted. The context must be stored *somewhere*, usually on the GC heap, which is why delegates depend on the GC. Except for scope delegates where it's guaranteed that the parent scope won't go out of scope before the delegate does, so the context pointer can just point to the stack where the parent's scope is stored. Unless I'm missing something obvious, I don't think value delegates would work well with D, because it's pretty important for delegates with the same parameter and return types to be interchangeable. I.e., two delegates with external type `int delegate(string)` must have the same fixed size and memory layout, regardless of the size of the context of one delegate vs. the other, otherwise you have a pretty serious problem at the language level. T -- Everybody talks about it, but nobody does anything about it! -- Mark TwainYour proposal is essentially a value delegate; the __context stuff makes that explicit, but it's not actually fundamentally different from the current scheme, it's just that the context becomes a value type rather than a reference type.Right, I honestly don't know how the context is passed for an alias parameter. The compiler must pass some sort of context pointer to the calling function, which then gets stored somehow. But it's all hidden. If it's hidden, it might as well be as big as needed.
Jun 22 2021
On 6/22/21 12:13 PM, H. S. Teoh wrote:Unless I'm missing something obvious, I don't think value delegates would work well with D, because it's pretty important for delegates with the same parameter and return types to be interchangeable. I.e., two delegates with external type `int delegate(string)` must have the same fixed size and memory layout, regardless of the size of the context of one delegate vs. the other, otherwise you have a pretty serious problem at the language level.This again, is a misunderstanding of my proposal. I'm not talking about altering delegates in any way. There are no delegates involved in `filter`. It is, instead, an inner struct with a hidden context pointer. Could just as well be a struct with context carried with it instead of allocated on the heap. Note that this generates 2 *different* `FilterResult` instances, because they are 2 different lambdas: ```d int[] arr = [1, 2, 3]; auto f1 = arr.filter!(v => v % 2); atuo f2 = arr.filter!(v => v % 2); static assert(!is(typeof(f1) == typeof(f2))); ``` I'm unclear how the compiler generates and passes the context, but I know that it uses the GC. However, it would be reasonable to allow functions to accept the context by value (if it makes sense). One way to do this is to work out whether you can do it by value, and use that if possible. Another way is to explicitly have different syntax to pass the context by value. I was hoping for a possible way forward where the compiler figures it all out for me. -Steve
Jun 22 2021
On Tue, Jun 22, 2021 at 03:56:03PM -0400, Steven Schveighoffer via Digitalmars-d wrote: [...]This again, is a misunderstanding of my proposal. I'm not talking about altering delegates in any way. There are no delegates involved in `filter`. It is, instead, an inner struct with a hidden context pointer. Could just as well be a struct with context carried with it instead of allocated on the heap.[...] Ohhh I see what you mean now. So essentially what you're saying is, whenever there's an alias parameter bound to a lambda, the compiler should, in effect, insert additional parameters to the template function that captures the context the lambda is bound to? IOW, this: auto filter(alias fun, R)(R range) { struct Voldemort { ... if (fun(range.front)) ... ... } return Voldemort(range); } int x = 2, y = 3; auto r = [1, 2, 3].filter!(e => e == x || e == y); gets lowered into something like this: struct __lambda_context { int x, y; } bool __lambda_fun(__lambda_context ctxt, int e) { return e == ctxt.x || e == ctxt.y; } auto __filter_instance(R)(__lambda_context ctxt, R range) { struct Voldemort { __lambda_context secretCtxt; ... if (__lambda_fun(secretCtxt, range.front)) ... ... } return Voldemort(ctxt, range); } int x = 2, y = 3; auto r = __filter_instance(__lambda_context(x, y), [1, 2, 3]); Am I right? __lambda_context may not necessarily carry a copy of the captured variables; possibly it could use pointers instead. Though care would have to be taken not to leave dangling pointers to out-of-scope local variables -- probably you'd have to make the references `scope` unless you copy them by value into the Voldemort struct. T -- Не дорог подарок, дорога любовь.
Jun 22 2021
On 6/22/21 5:31 PM, H. S. Teoh wrote:On Tue, Jun 22, 2021 at 03:56:03PM -0400, Steven Schveighoffer via Digitalmars-d wrote: [...][snip]This again, is a misunderstanding of my proposal. I'm not talking about altering delegates in any way. There are no delegates involved in `filter`. It is, instead, an inner struct with a hidden context pointer. Could just as well be a struct with context carried with it instead of allocated on the heap.[...]Am I right?Yes, exactly! The only tricky part is that the filter function has to explicitly accept the context so it can tell the compiler where to put the context. Essentially, it gives a place to accept the context and has the compiler give it a way to forward that context to the lambda function(s). It may be trickier than I originally wrote, but the goal is to avoid having to do any work as a user (the library can be instrumented as much as we want), and the compiler just does the right thing. -Steve
Jun 23 2021
On Tuesday, 22 June 2021 at 16:13:06 UTC, H. S. Teoh wrote:The context must be stored *somewhere*, usually on the GC heap, which is why delegates depend on the GC. Except for scope delegates where it's guaranteed that the parent scope won't go out of scope before the delegate does, so the context pointer can just point to the stack where the parent's scope is stored.The way C++ works is that a lambda (which is essentially a template class) when you pass it to a function/method it will be converted to an std::function. Inside of std::function it will store context data on the heap. However, it has some "SSO" capability meaning if the captures are few and small it might fit into meta data of std::function. Essentially this is the same SSO (short string optimization) strategy for std::string. Not sure if this helps but for small lambdas that does simple things like in the initial example, the context could easily be stored in the struct. Now the problem is in D the delegate is very small, while in C++ it is probably more going on like reference counting making std::function bigger than in D. Making structs bigger isn't always that popular.
Jun 22 2021
On Monday, 21 June 2021 at 20:01:27 UTC, Steven Schveighoffer wrote: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?You are asking for C++ lambdas? No need for a new type of template parameter.
Jun 21 2021
On 6/22/21 1:57 AM, Ola Fosheim Grostad wrote:On Monday, 21 June 2021 at 20:01:27 UTC, Steven Schveighoffer wrote:I was kinda hoping for the compiler to assist in the generation and management of the context data since it's already doing it. The receiving function just needs a variable to receive and pass the context data to the lambda. -SteveThe 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?You are asking for C++ lambdas? No need for a new type of template parameter.
Jun 22 2021
On Tuesday, 22 June 2021 at 14:49:34 UTC, Steven Schveighoffer wrote:I was kinda hoping for the compiler to assist in the generation and management of the context data since it's already doing it. The receiving function just needs a variable to receive and pass the context data to the lambda.Off the top of my head: I think it should just be an ordinary template parameter. Then you can let the compiler do the optimization if is possible without changing the semantics. That way it isn't a language change. So you could instead just introduce a UDA that signals that a compiler should issue a warning if it cannot do the optimization? Or am I wrong?
Jun 23 2021
On 6/23/21 7:04 AM, Ola Fosheim Grøstad wrote:On Tuesday, 22 June 2021 at 14:49:34 UTC, Steven Schveighoffer wrote:Well, the library function needs to signify to the compiler that the parameter is meant to accept the value context. Otherwise, really odd things might happen with existing template parameters.I was kinda hoping for the compiler to assist in the generation and management of the context data since it's already doing it. The receiving function just needs a variable to receive and pass the context data to the lambda.Off the top of my head: I think it should just be an ordinary template parameter. Then you can let the compiler do the optimization if is possible without changing the semantics. That way it isn't a language change.So you could instead just introduce a UDA that signals that a compiler should issue a warning if it cannot do the optimization? Or am I wrong?A UDA might work. -Steve
Jun 23 2021
On Wednesday, 23 June 2021 at 13:43:21 UTC, Steven Schveighoffer wrote:A UDA might work.Ok, so you use a templated filter function with a regular delegate for the parameter, but the delegate parameter is adorned with __nogc_lambda. Then the compiler turns the __nogc_lambda delegate into a context. Compilers that don't support __lambda will simply gc-allocate the lambda? So it is a nonbreaking extension. Is that good enough? If so, then we can do it without a DIP.
Jun 23 2021
On 21/06/2021 23:01, Steven Schveighoffer wrote: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. *snip*It does come up every so often. You can also follow the links here. https://forum.dlang.org/post/qnigarkuxxnqwdernhzv forum.dlang.org Maybe the zip+repeat trick should be streamlined a bit.
Jun 21 2021
On 6/22/21 2:20 AM, rm wrote:It does come up every so often. You can also follow the links here. https://forum.dlang.org/post/qnigarkuxxnqwdernhzv forum.dlang.orgThanks for the link, I didn't remember this (it just happened a few months ago too!)Maybe the zip+repeat trick should be streamlined a bit.Yeah, I'm suggesting to avoid having to specify explicitly on the call side how to pass the data. The compiler could figure it out for me, and I get ` nogc` `filter` for free. -Steve
Jun 22 2021
On Monday, 21 June 2021 at 20:01:27 UTC, Steven Schveighoffer wrote: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). -SteveWhy can't it be implemented by using templates? - Alex
Jun 22 2021
On 6/22/21 8:38 PM, 12345swordy wrote:Why can't it be implemented by using templates?Oh it can, I did in the original message! It just doesn't look as nice. -Steve
Jun 23 2021