www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Prime sieve language race

reply Bastiaan Veelo <Bastiaan Veelo.net> writes:
Dave's Garage is hosting a race to find the fastest among 45 
programming languages. I just watched the [first episode covering 
Pascal, Delphi and Ada](https://youtu.be/tQtFdsEcK_s). There is 
currently [one contribution in D by 
Eagerestwolf](https://github.com/PlummersSoftwareLLC/Primes/tree/drag-race
PrimeD/solution_1). If you can make it faster or want to submit a parallel
version, I think they still accept
[contributions](https://github.com/PlummersSoftwareLLC/Primes/blob/drag-race/CONTRIBUTING.md).

I don't know when the episode on D will be made, and it might be 
interesting if multiple solutions using different styles are 
available. Anyway, tweaks will be accepted until a final 
comparison in the last episode.

-- Bastiaan.
Jul 04
next sibling parent reply SealabJaster <sealabjaster gmail.com> writes:
On Sunday, 4 July 2021 at 15:31:31 UTC, Bastiaan Veelo wrote:
 ...
Decided to give it a go. Faithful but is slightly more D-ish https://github.com/PlummersSoftwareLLC/Primes/pull/292
Jul 04
next sibling parent SealabJaster <sealabjaster gmail.com> writes:
On Sunday, 4 July 2021 at 22:35:44 UTC, SealabJaster wrote:
 On Sunday, 4 July 2021 at 15:31:31 UTC, Bastiaan Veelo wrote:
 ...
Decided to give it a go. Faithful but is slightly more D-ish https://github.com/PlummersSoftwareLLC/Primes/pull/292
The more I think about it though, maybe this should've just been an improvement over solution 1, instead of putting it as a second solution.
Jul 04
prev sibling parent reply Bastiaan Veelo <Bastiaan Veelo.net> writes:
On Sunday, 4 July 2021 at 22:35:44 UTC, SealabJaster wrote:
 On Sunday, 4 July 2021 at 15:31:31 UTC, Bastiaan Veelo wrote:
 ...
Decided to give it a go. Faithful but is slightly more D-ish https://github.com/PlummersSoftwareLLC/Primes/pull/292
Merged here: https://github.com/PlummersSoftwareLLC/Primes/tree/drag-race/PrimeD/solution_2 I like it! A few points: 1. The output in the readme shows `Valid: false`, which worries me. I didn't try your solution myself yet. 2. Not sure if this was clearly stated in the rules, but he [says that the sieve size must be a run time value](https://youtu.be/Yl9OegOorYM?t=532). I hope this doesn't disqualify your solution. Further some nit-picks, feel free to ignore: 3. Line 23 `// it also allows D to write more "file-portable" code.` Not sure what you mean by this. Worth noting however is that the import only happens iff the template is instantiated, which is nice. 4. Line 38: Did you mean to leave `(citation needed)` in there? 5. `printResults` can be made ` safe` by means of a nested ` trusted` wrapper ([ref](https://forum.dlang.org/post/pvadtwqblgclttzesxeg forum.dlang.org)): ```d // If not called from multiple threads, this can be trusted. static File trustedStderr() trusted { return stderr; } ``` Out of curiosity, do you know how your solution compares with the first one performance wise, roughly? Thanks for your submission! -- Bastiaan.
Jul 13
next sibling parent reply ag0aep6g <anonymous example.com> writes:
On Tuesday, 13 July 2021 at 19:06:12 UTC, Bastiaan Veelo wrote:
     // If not called from multiple threads, this can be trusted.
I.e., it can't be trusted. When a function needs a comment that explains how to call it safely, then it's an system function.
Jul 13
next sibling parent Bastiaan Veelo <Bastiaan Veelo.net> writes:
On Tuesday, 13 July 2021 at 19:45:48 UTC, ag0aep6g wrote:
 On Tuesday, 13 July 2021 at 19:06:12 UTC, Bastiaan Veelo wrote:
     // If not called from multiple threads, this can be 
 trusted.
I.e., it can't be trusted. When a function needs a comment that explains how to call it safely, then it's an system function.
Then `printResults` can't be trusted either. Or change the comment s/If/Since/?
Jul 13
prev sibling parent reply Sebastiaan Koppe <mail skoppe.eu> writes:
On Tuesday, 13 July 2021 at 19:45:48 UTC, ag0aep6g wrote:
 On Tuesday, 13 July 2021 at 19:06:12 UTC, Bastiaan Veelo wrote:
     // If not called from multiple threads, this can be 
 trusted.
I.e., it can't be trusted. When a function needs a comment that explains how to call it safely, then it's an system function.
Why can't non-threadsafe functions be safe? Because it might corrupt memory? On a static function that is probably the right thing to do. But what about with a member function? I would argue it isn't.
Jul 14
parent reply ag0aep6g <anonymous example.com> writes:
On 14.07.21 09:08, Sebastiaan Koppe wrote:
 On Tuesday, 13 July 2021 at 19:45:48 UTC, ag0aep6g wrote:
 On Tuesday, 13 July 2021 at 19:06:12 UTC, Bastiaan Veelo wrote:
     // If not called from multiple threads, this can be trusted.
I.e., it can't be trusted. When a function needs a comment that explains how to call it safely, then it's an system function.
Why can't non-threadsafe functions be safe? Because it might corrupt memory? On a static function that is probably the right thing to do. But what about with a member function? I would argue it isn't.
You would argue that a function that might corrupt memory should be trusted when it's a member function? If the function might corrupt memory, it must be system. This is how I understand the comment. If the function cannot corrupt memory (even when called from multiple threads), the comment is very misleading. If the function cannot possibly be called from multiple threads (no idea how that would work), the comment is also misleading.
Jul 14
parent reply Sebastiaan Koppe <mail skoppe.eu> writes:
On Wednesday, 14 July 2021 at 12:08:29 UTC, ag0aep6g wrote:
 On 14.07.21 09:08, Sebastiaan Koppe wrote:
 Why can't non-threadsafe functions be  safe? Because it might 
 corrupt memory?
 
 On a static function that is probably the right thing to do. 
 But what about with a member function? I would argue it isn't.
You would argue that a function that might corrupt memory should be trusted when it's a member function?
Because member functions are harder to call from multiple threads than static functions are. For one, you will have to get the object on two threads first. Most functions that do that require a shared object, which requires a diligent programmer to do the casting.
Jul 14
next sibling parent reply ag0aep6g <anonymous example.com> writes:
On Wednesday, 14 July 2021 at 19:10:55 UTC, Sebastiaan Koppe 
wrote:
 Because member functions are harder to call from multiple 
 threads than static functions are. For one, you will have to 
 get the object on two threads first. Most functions that do 
 that require a shared object, which requires a diligent 
 programmer to do the casting.
The object isn't necessarily the thing that is being shared. A method can be accessing some `__gshared` global just like a static function can.
Jul 14
next sibling parent reply Sebastiaan Koppe <mail skoppe.eu> writes:
On Wednesday, 14 July 2021 at 20:45:05 UTC, ag0aep6g wrote:
 On Wednesday, 14 July 2021 at 19:10:55 UTC, Sebastiaan Koppe 
 wrote:
 Because member functions are harder to call from multiple 
 threads than static functions are. For one, you will have to 
 get the object on two threads first. Most functions that do 
 that require a shared object, which requires a diligent 
 programmer to do the casting.
The object isn't necessarily the thing that is being shared. A method can be accessing some `__gshared` global just like a static function can.
Of course, there is always a loophole somewhere. But does that all imply that we have to make all non-threadsafe functions system? How can we every be safe?
Jul 15
next sibling parent ag0aep6g <anonymous example.com> writes:
On Thursday, 15 July 2021 at 13:10:44 UTC, Sebastiaan Koppe wrote:
 But does that all imply that we have to make all non-threadsafe 
 functions  system? How can we every be  safe?
Write thread-safe code?
Jul 15
prev sibling parent reply Petar Kirov [ZombineDev] <petar.p.kirov gmail.com> writes:
On Thursday, 15 July 2021 at 13:10:44 UTC, Sebastiaan Koppe wrote:
 But does that all imply that we have to make all non-threadsafe 
 functions  system? How can we every be  safe?
TL;DR of my previous post is "no" as the answer to your question. It's perfectly fine to develop/use ` safe` non-`shared` code. That should actually be the default for most projects. You should mark code as ` system` only if it implicitly uses shared data (no matter if it is actually marked as `shared`) without internal synchronization. In the case of [`trustedStdout`][0] it really should be ` system` as from the outside (it's signature) it looks like it gives access to a thread-local object, but it's actually returning an implicitly-shared one (meaning it has shared global mutable state, even though it's not marked as `shared`). Another TL;DR: * ` safe shared` -> thread-safe * ` safe` and not `shared` -> safe in single-threaded code; unusable if `shared`, unless externally synchronized correctly * ` system` and not `shared` -> beware! Tricky to use safely even with external synchronization, as there could be other code that uses this without any synchronization outside of your control [0]: https://github.com/dlang/phobos/blob/v2.097.0/std/stdio.d#L4136
Jul 15
parent Petar Kirov [ZombineDev] <petar.p.kirov gmail.com> writes:
On Thursday, 15 July 2021 at 17:17:53 UTC, Petar Kirov 
[ZombineDev] wrote:
 [..] You should mark code as  system only if it implicitly uses 
 shared data [..]
I mean "only" in the context of multi-threading, other memory/type safety issues not withstanding.
Jul 15
prev sibling parent reply Paul Backus <snarwin gmail.com> writes:
On Wednesday, 14 July 2021 at 20:45:05 UTC, ag0aep6g wrote:
 On Wednesday, 14 July 2021 at 19:10:55 UTC, Sebastiaan Koppe 
 wrote:
 Because member functions are harder to call from multiple 
 threads than static functions are. For one, you will have to 
 get the object on two threads first. Most functions that do 
 that require a shared object, which requires a diligent 
 programmer to do the casting.
The object isn't necessarily the thing that is being shared. A method can be accessing some `__gshared` global just like a static function can.
` safe` code can't access `__gshared` data.
Jul 15
parent ag0aep6g <anonymous example.com> writes:
On Thursday, 15 July 2021 at 13:12:34 UTC, Paul Backus wrote:
 ` safe` code can't access `__gshared` data.
We're talking about system code and whether it can be trusted.
Jul 15
prev sibling parent reply Petar Kirov [ZombineDev] <petar.p.kirov gmail.com> writes:
On Wednesday, 14 July 2021 at 19:10:55 UTC, Sebastiaan Koppe 
wrote:
 On Wednesday, 14 July 2021 at 12:08:29 UTC, ag0aep6g wrote:
 On 14.07.21 09:08, Sebastiaan Koppe wrote:
 Why can't non-threadsafe functions be  safe? Because it might 
 corrupt memory?
 
 On a static function that is probably the right thing to do. 
 But what about with a member function? I would argue it isn't.
You would argue that a function that might corrupt memory should be trusted when it's a member function?
Because member functions are harder to call from multiple threads than static functions are. For one, you will have to get the object on two threads first. Most functions that do that require a shared object, which requires a diligent programmer to do the casting.
While in terms of program design, encapsulation is very related to safety (as it allows to hide unsafe interfaces that may violate program invariants), from a D language point of view, they're completely orthogonal concepts. An argument, can only be made in favor of allowing ` trusted` nested functions with non-` safe` interface (which have the strongest form of encapsulation, just by means of lexical scoping) when they improve readability considerably compared to IIFE ( trusted lambda idiom). The problem with `std.stdio : std{in,out,err}` is they ought to be defined (conceptually) as `shared Atomic!File`, where `File` is essentially a wrapper around `SharedPtr!FileState` (and `SharedPtr` does atomic ref-counting, if it's `shared`) and until then, they shouldn't be ` trusted`, unless the program is single-threaded.
Jul 15
parent reply Sebastiaan Koppe <mail skoppe.eu> writes:
On Thursday, 15 July 2021 at 12:35:29 UTC, Petar Kirov 
[ZombineDev] wrote:
 On Wednesday, 14 July 2021 at 19:10:55 UTC, Sebastiaan Koppe
 Because member functions are harder to call from multiple 
 threads than static functions are. For one, you will have to 
 get the object on two threads first. Most functions that do 
 that require a shared object, which requires a diligent 
 programmer to do the casting.
The problem with `std.stdio : std{in,out,err}` is they ought to be defined (conceptually) as `shared Atomic!File`, where `File` is essentially a wrapper around `SharedPtr!FileState` (and `SharedPtr` does atomic ref-counting, if it's `shared`) and until then, they shouldn't be ` trusted`, unless the program is single-threaded.
Yes that is the sensible thing to do. But I am not sure that is the right thing. I am afraid that it will lead to the conclusion that everything needs to be shared, because who is going to stop someone from taking your struct/class/function, moving it over to another thread and then complain it corrupts memory while it was advertised as having a safe interface?
Jul 15
next sibling parent ag0aep6g <anonymous example.com> writes:
On Thursday, 15 July 2021 at 13:16:01 UTC, Sebastiaan Koppe wrote:
 Yes that is the sensible thing to do. But I am not sure that is 
 the right thing. I am afraid that it will lead to the 
 conclusion that everything needs to be shared, because who is 
 going to stop someone from taking your struct/class/function, 
 moving it over to another thread and then complain it corrupts 
 memory while it was advertised as having a  safe interface?
If you share an object between threads, it must be typed as `shared`. Then someone can only call those methods that are also marked `shared`. Those methods are thread-safe. If someone casts `shared` away, it's on them to ensure thread-safety.
Jul 15
prev sibling parent Petar Kirov [ZombineDev] <petar.p.kirov gmail.com> writes:
On Thursday, 15 July 2021 at 13:16:01 UTC, Sebastiaan Koppe wrote:
 On Thursday, 15 July 2021 at 12:35:29 UTC, Petar Kirov 
 [ZombineDev] wrote:
 On Wednesday, 14 July 2021 at 19:10:55 UTC, Sebastiaan Koppe
 Because member functions are harder to call from multiple 
 threads than static functions are. For one, you will have to 
 get the object on two threads first. Most functions that do 
 that require a shared object, which requires a diligent 
 programmer to do the casting.
The problem with `std.stdio : std{in,out,err}` is they ought to be defined (conceptually) as `shared Atomic!File`, where `File` is essentially a wrapper around `SharedPtr!FileState` (and `SharedPtr` does atomic ref-counting, if it's `shared`) and until then, they shouldn't be ` trusted`, unless the program is single-threaded.
Yes that is the sensible thing to do. But I am not sure that is the right thing. I am afraid that it will lead to the conclusion that everything needs to be shared, because who is going to stop someone from taking your struct/class/function, moving it over to another thread and then complain it corrupts memory while it was advertised as having a safe interface?
Not quite. If an aggregate has no methods marked as `shared`, it means that in essence it's not designed to be shared across threads (i.e it's not thread-safe). Just like `const` methods define the API of `const` object instances, `shared` methods define the API of `shared` objects. While it can be useful to overload methods based on the `this` type qualifier (e.g. I added `shared` overloads to the `lock`, `unlock` and `tryLock` methods of [`core.sync.mutex : Mutex`][0] (*)), it's not strictly necessary. It's perfectly possible to have a class which has one set of functions of single-thread use and a complete separate set of thread-safe functions. As an example, a simple non-thread-safe queue class can have `front`, `push`, `pop` and `empty` methods, while a thread-safe variant will instead have `tryGetFront`, `tryPush`, `tryPop` (and no `empty`) methods.
 I am afraid that it will lead to the conclusion that everything 
 needs to be shared
(at least, in my experience), where you don't know whether your class may be shared across threads, so you either find out eventually the hard way (via bug reports), or (e.g. if requested by code reviewers) you go in and preemptively add locks all over the code (usually not tested well, since you your initial use-case didn't involve sharing the object across threads). This is not the case in D. If your aggregate doesn't have `shared` methods it means that it must not be `shared`, plain and simple. That's why `__gshared` should be avoided - it shares both thread-safe and non-thread-safe objects across threads. A `__gshared` `Mutex` will work just fine (as the underlying Posix/Win32 primitives are obviously designed support it), but other types, like D's associative arrays would certainly go kaboom, if access to them is not *synchronized externally* (**). In case of Phobos, `std.stdio : std{in,out,err}` should really be made thread-safe (you can find issues in bugzilla), as the whole idea of making them global mutable properties is to allow any thread to redirect them at any point of time. Whether that's a good idea is a separate topic, but it was certainly an intended case. (*) `core.sync.mutex : Mutex.{lock, unlock, tryLock}` really should have been `shared safe nothrow nogc` from the beginning, but hey better late, then never :) I considered removing the non-`shared` overloads, but I decided against, as that would have been a breaking change. That said, once we have enough high-quality APIs in Phobos to allow ergonomic use of `shared` (i.e. not requiring people to cast-away `shared` all over the place), we should consider deprecating them (the non-`shared` overloads of `lock`/`unlock`/`tryLock`). (**) Another way to discuss `shared` is to think in terms of *internal* and *external synchronization*. If a method is `shared`, it follows that access to the underlying object is *internally synchronized*, i.e. you don't need an external mutex to guard it. And vice versa - if the methods are not `shared`, it means that you need to use external synchronization, and only then (assuming you have implemented it correctly), you can cast away `shared` and freely call the non-`shared` methods inside the scope of the lock. See Rust's [`Mutex`][1] and more specifically the [`MutexGuard`][2] types for a good example of this technique. Given a type like `Rust`'s `MutexGuard`, casting-away `shared` should really not be done in user-code - the idea is that the `MutexGuard` will give you a safe `scope`-ed access to a head-un-`shared` type (given `shared(SomeType**)` it will give you `scope shared(SomeType*)*`). P.S. I use the term "method" when I mean non-static member function, and "aggregate" when I mean `struct`, `class`, or `interface` type. [0]: https://dlang.org/phobos/core_sync_mutex.html [1]: https://doc.rust-lang.org/std/sync/struct.Mutex.html [2]: https://doc.rust-lang.org/std/sync/struct.MutexGuard.html
Jul 15
prev sibling next sibling parent Bastiaan Veelo <Bastiaan Veelo.net> writes:
On Tuesday, 13 July 2021 at 19:06:12 UTC, Bastiaan Veelo wrote:
 2. Not sure if this was clearly stated in the rules, but he 
 [says that the sieve size must be a run time 
 value](https://youtu.be/Yl9OegOorYM?t=532). I hope this doesn't 
 disqualify your solution.
Not disqualified, but rendered [unfaithful](https://github.com/PlummersSoftwareLLC/Primes/blob/drag-race/CONTRIBUTING.md#faithfulness). IIRC unfaithful solutions can still be discussed in the video and therefore be interesting, but the benchmark is about faithful solutions... --Bastiaan.
Jul 13
prev sibling parent reply SealabJaster <sealabjaster gmail.com> writes:
On Tuesday, 13 July 2021 at 19:06:12 UTC, Bastiaan Veelo wrote:
 1. The output in the readme shows `Valid: false`, which worries 
 me. I didn't try your solution myself yet.
Oop. So what happened there is, I updated the README while `validateResults` was broken, and completely forgot to fix the README after fixing `validateResults`. I can assure you it shows `Valid: true` now!
 2. Not sure if this was clearly stated in the rules, but he 
 [says that the sieve size must be a run time 
 value](https://youtu.be/Yl9OegOorYM?t=532). I hope this doesn't 
 disqualify your solution.
oooh. It's easy to read over but it does seem to say that:
 The sieve size and corresponding prime candidate memory buffer 
 (or language equivalent) are set/allocated dynamically at 
 runtime. The size of the memory buffer must correspond to the 
 size of the sieve.
It'll mean that the solution can't be marked as `faithful: yes` anymore, I'll open a PR soon to make sure that's fixed. Or maybe I'll update the code slightly to allow both a runtime and compile-time set sieve size >:3 While the Sieve does do the computations at runtime, the sieve size is a compile-time constant, and the buffer is static (technically one could argue that it *is* dynamically allocated due to being in a class >:D ).
 3. Line 23 `// it also allows D to write more "file-portable" 
 code.` Not sure what you mean by this. Worth noting however is 
 that the import only happens iff the template is instantiated, 
 which is nice.
Basically, if you use scoped imports it tends to be a lot easier to move a piece of code between different files. A lot of the time you can get away with a simple cut+paste and it can just work. Hence, portable across files.
 4. Line 38: Did you mean to leave `(citation needed)` in there?
Perhaps ;)
 5. `printResults` can be made ` safe` by means of a nested 
 ` trusted` wrapper 
 ([ref](https://forum.dlang.org/post/pvadtwqblgclttzesxeg forum.dlang.org)):
 ```d
     // If not called from multiple threads, this can be trusted.
     static File trustedStderr()  trusted
     {
         return stderr;
     }
 ```
It really doesn't feel right to do that. I didn't use the ` trusted lambda` hack for a reason.
 Out of curiosity, do you know how your solution compares with 
 the first one performance wise, roughly?
[1] is for the first solution, and [2] is for the second one. Because the second solution has some compile-time things going on, it's doing a fair amount less work: not needing to allocate the list of sieve sizes; `validateResults` not needing to do a lookup; one less level of indirection because it uses a static array; etc. I also set some compiler flags in dub.sdl, which doesn't actually seem to change solution_1 all that much if I apply the same flags to it. As I mentioned in the PR as well, LDC is capable of inlining ranges, so even though this code is more idiomatic, it compiles like it was written in a more traditional C style. [1] https://pastebin.com/Q0UxibTi [2] https://pastebin.com/tmXDwejS
Jul 13
next sibling parent Bastiaan Veelo <Bastiaan Veelo.net> writes:
On Tuesday, 13 July 2021 at 21:02:55 UTC, SealabJaster wrote:

 I can assure you it shows `Valid: true` now!
Phew! :-)
 Or maybe I'll update the code slightly to allow both a runtime 
 and compile-time set sieve size >:3
That would be nice.
 While the Sieve does do the computations at runtime, the sieve 
 size is a compile-time constant, and the buffer is static 
 (technically one could argue that it *is* dynamically allocated 
 due to being in a class >:D ).
Heh yes. Also, he said something like “as if you would write an API”. I like templated APIs… I saw two comments regarding dynamic sieve size. One of them was an entire stack-based implementation, to which he responded not seeing a problem with that. Another suggested using `alloca`.
 3. Line 23 `// it also allows D to write more "file-portable" 
 code.` Not sure what you mean by this. Worth noting however is 
 that the import only happens iff the template is instantiated, 
 which is nice.
Basically, if you use scoped imports it tends to be a lot easier to move a piece of code between different files. A lot of the time you can get away with a simple cut+paste and it can just work.
Agreed. Thanks.
 Out of curiosity, do you know how your solution compares with 
 the first one performance wise, roughly?
[1] is for the first solution, and [2] is for the second one.
[….]
 [1] https://pastebin.com/Q0UxibTi
 [2] https://pastebin.com/tmXDwejS
Those are nice improvements! Glad you took the time. — Bastiaan.
Jul 13
prev sibling parent reply SealabJaster <sealabjaster gmail.com> writes:
On Tuesday, 13 July 2021 at 21:02:55 UTC, SealabJaster wrote:
 Because the second solution has some compile-time things going 
 on, it's doing a fair amount less work: not needing to allocate 
 the list of sieve sizes; `validateResults` not needing to do a 
 lookup; one less level of indirection because it uses a static 
 array; etc.
Some corrections:
 not needing to allocate the list of sieve sizes
This actually doesn't matter since there's no need to do so until the primes are calculated, so time taken doesn't need to be measured.
 `validateResults` not needing to do a lookup
This function isn't part of the measurement either.
Jul 13
parent reply SealabJaster <sealabjaster gmail.com> writes:
On Tuesday, 13 July 2021 at 22:11:34 UTC, SealabJaster wrote:
 ...
https://github.com/PlummersSoftwareLLC/Primes/pull/407
Jul 13
parent reply SealabJaster <sealabjaster gmail.com> writes:
On Tuesday, 13 July 2021 at 23:23:56 UTC, SealabJaster wrote:
 On Tuesday, 13 July 2021 at 22:11:34 UTC, SealabJaster wrote:
 ...
https://github.com/PlummersSoftwareLLC/Primes/pull/407
_ Finally decided to run the full benchmark. On my weak linux machine, it comes out at 68(CT) 76(RT) out of 191 for the single threaded version. For the multithreaded we're at 39(CT) and 43(RT) out of 52. I've also managed to get WSL to run it now, so I'll report back on the results from my main machine.
Jul 29
parent SealabJaster <sealabjaster gmail.com> writes:
On Thursday, 29 July 2021 at 07:16:41 UTC, SealabJaster wrote:
 ...
Scratch WSL, it's super weird and kind of awful. Anyway, another PR: https://github.com/PlummersSoftwareLLC/Primes/pull/541
Jul 29
prev sibling parent reply Andrea Fontana <nospam example.org> writes:
On Sunday, 4 July 2021 at 15:31:31 UTC, Bastiaan Veelo wrote:
 I don't know when the episode on D will be made, and it might 
 be interesting if multiple solutions using different styles are 
 available. Anyway, tweaks will be accepted until a final 
 comparison in the last episode.

 -- Bastiaan.
Why not ldc2 instead of gdc?
Jul 16
parent Bastiaan Veelo <Bastiaan Veelo.net> writes:
On Friday, 16 July 2021 at 08:07:27 UTC, Andrea Fontana wrote:
 Why not ldc2 instead of gdc?
Solution2 uses ldc2. — Bastiaan.
Jul 16