digitalmars.D - Prime sieve language race
- Bastiaan Veelo (10/10) Jul 04 2021 Dave's Garage is hosting a race to find the fastest among 45
- SealabJaster (3/4) Jul 04 2021 Decided to give it a go. Faithful but is slightly more D-ish
- SealabJaster (4/8) Jul 04 2021 The more I think about it though, maybe this should've just been
- Bastiaan Veelo (30/34) Jul 13 2021 Merged here:
- ag0aep6g (3/4) Jul 13 2021 I.e., it can't be trusted. When a function needs a comment that
- Bastiaan Veelo (3/8) Jul 13 2021 Then `printResults` can't be trusted either. Or change the
- Sebastiaan Koppe (5/10) Jul 14 2021 Why can't non-threadsafe functions be @safe? Because it might
- ag0aep6g (9/21) Jul 14 2021 You would argue that a function that might corrupt memory should be
- Sebastiaan Koppe (6/14) Jul 14 2021 Because member functions are harder to call from multiple threads
- ag0aep6g (5/10) Jul 14 2021 The object isn't necessarily the thing that is being shared. A
- Sebastiaan Koppe (4/14) Jul 15 2021 Of course, there is always a loophole somewhere.
- ag0aep6g (2/4) Jul 15 2021 Write thread-safe code?
- Petar Kirov [ZombineDev] (20/22) Jul 15 2021 TL;DR of my previous post is "no" as the answer to your question.
- Petar Kirov [ZombineDev] (4/6) Jul 15 2021 I mean "only" in the context of multi-threading, other
- Paul Backus (2/12) Jul 15 2021 `@safe` code can't access `__gshared` data.
- ag0aep6g (2/3) Jul 15 2021 We're talking about @system code and whether it can be @trusted.
- Petar Kirov [ZombineDev] (17/32) Jul 15 2021 While in terms of program design, encapsulation is very related
- Sebastiaan Koppe (8/20) Jul 15 2021 Yes that is the sensible thing to do. But I am not sure that is
- ag0aep6g (5/11) Jul 15 2021 If you share an object between threads, it must be typed as
- Petar Kirov [ZombineDev] (67/90) Jul 15 2021 Not quite. If an aggregate has no methods marked as `shared`, it
- Bastiaan Veelo (7/11) Jul 13 2021 Not disqualified, but rendered
- SealabJaster (36/63) Jul 13 2021 Oop. So what happened there is, I updated the README while
- Bastiaan Veelo (12/32) Jul 13 2021 That would be nice.
- SealabJaster (6/13) Jul 13 2021 This actually doesn't matter since there's no need to do so until
- SealabJaster (2/3) Jul 13 2021 https://github.com/PlummersSoftwareLLC/Primes/pull/407
- SealabJaster (7/10) Jul 29 2021 @_@ Finally decided to run the full benchmark.
- SealabJaster (4/5) Jul 29 2021 Scratch WSL, it's super weird and kind of awful.
- Andrea Fontana (2/7) Jul 16 2021 Why not ldc2 instead of gdc?
- Bastiaan Veelo (3/4) Jul 16 2021 Solution2 uses ldc2.
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 2021
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 2021
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: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....Decided to give it a go. Faithful but is slightly more D-ish https://github.com/PlummersSoftwareLLC/Primes/pull/292
Jul 04 2021
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: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....Decided to give it a go. Faithful but is slightly more D-ish https://github.com/PlummersSoftwareLLC/Primes/pull/292
Jul 13 2021
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 2021
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:Then `printResults` can't be trusted either. Or change the comment s/If/Since/?// 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 2021
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: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.// 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 14 2021
On 14.07.21 09:08, Sebastiaan Koppe wrote:On Tuesday, 13 July 2021 at 19:45:48 UTC, ag0aep6g wrote: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.On Tuesday, 13 July 2021 at 19:06:12 UTC, Bastiaan Veelo 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.// 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 14 2021
On Wednesday, 14 July 2021 at 12:08:29 UTC, ag0aep6g wrote:On 14.07.21 09:08, 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.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?
Jul 14 2021
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 2021
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: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?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 15 2021
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 2021
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 2021
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 2021
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:` safe` code can't access `__gshared` data.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 15 2021
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 2021
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: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.On 14.07.21 09:08, 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.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?
Jul 15 2021
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 KoppeYes 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?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.
Jul 15 2021
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 2021
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: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.On Wednesday, 14 July 2021 at 19:10:55 UTC, Sebastiaan KoppeYes 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?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.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 2021
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 2021
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 2021
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 >:3That 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`.Agreed. Thanks.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.[….]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/tmXDwejSThose are nice improvements! Glad you took the time. — Bastiaan.
Jul 13 2021
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 sizesThis 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 lookupThis function isn't part of the measurement either.
Jul 13 2021
On Tuesday, 13 July 2021 at 22:11:34 UTC, SealabJaster wrote:...https://github.com/PlummersSoftwareLLC/Primes/pull/407
Jul 13 2021
On Tuesday, 13 July 2021 at 23:23:56 UTC, SealabJaster wrote:On Tuesday, 13 July 2021 at 22:11:34 UTC, SealabJaster wrote:_ 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....https://github.com/PlummersSoftwareLLC/Primes/pull/407
Jul 29 2021
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 2021
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 2021
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 2021