HNHacker News
TopNewBestAskShowJobs

neonsunset

2,773 karma · joined November 9, 2019

Someone who stares into HN void

C#/.NET, a bit of Rust, F# and asm, performance analysis and (predominantly low-level) optimization.

Feel free to contact me if you have any questions or are curious about these or adjacent topics.

https://github.com/neon-sunset

https://x.com/_neonsunset

neon-sunset_at_outlook_com

submissionscomments
neonsunset··on Making Video Games in 2025 (without an engine)
.NET also now has an (amazing) alternate low-pause/effectively pauseless GC: https://github.com/VSadov/Satori

Builds: https://github.com/hez2010/Satori/releases

how to use? do self-contained publish (but not single file), replace 3 files in the folder with the one from Satori release you can check if it's in use with GC.GetConfigurationVariables().ContainsKey("SatoriGC")

It is a far, far superior experience to touching anything C++.

neonsunset··on Clean Coder: The Dark Path (2017)
Default value is only relevant for structs. You can setup an analyzer rule to ban this for structs which have, say, an empty constructor. It’s a similar problem to Go and C++ except it’s worse in the latter two.
neonsunset··on An Honest Review of Go (2025)
Even back in .NET Core 3.1 days C# had more than competitive performance profile with Go, and _much_ better multi-core scaling at allocation-heavy workloads.

It is disingenuous to say that whatever it ships with is huge also.

The common misconception by the industry that AOT is optimal and desired in server workloads is unfortunate. The deployment model (single slim binary vs many files vs host-dependent) is completely unrelated to whether the application utilizes JIT or AOT. Even with carefully gathered profile, Go produces much worse compiler output for something as trivial as hashmap lookup in comparison to .NET (or JVM for that matter).

neonsunset··on So, you want to chunk really fast?
.NET's string.Split implementation is very close to what the article showcases, even 3-character limit is there: https://github.com/dotnet/runtime/blob/main/src/libraries/Sy...
neonsunset··on Approachable Swift Concurrency
C# :)

    async Task<User> FetchUser(int id, HttpClient http, CancellationToken token)
    {
        var addr = $"https://api.example.com/users/{id}";
        var user = await http.GetFromJsonAsync<User>(addr, token);
        return user ?? throw new Exception("User not found");
    }
neonsunset··on Unity's Mono problem: C# code runs slower than it should
Just don't ship to PlayStation and discourage others until Sony changes (is forced to) policy.
neonsunset··on Go-boot: bare metal Go UEFI boot manager
I saw the email, and thanks. This is okay - I did not exercise (nor anyone should) good impulse control when dealing with bad faith arguments, which inevitably led to an account ban. Either way, Merry Christas!
neonsunset··on Go-boot: bare metal Go UEFI boot manager
What? NRTs are used everywhere with WarningAsErrors:nullable also gaining popularity. Whatever environment you are dealing with C# in, if it’s the opposite I suggest getting away from that ASAP.
neonsunset··on Rue: Higher level than Rust, lower level than Go
FWIW JIT is rarely an issue, and enables strong optimizations not available in AOT (it has its own, but JIT is overall much better for throughput). RyuJIT can do the same speculative optimizations OpenJDK Hotspot does except the language has fewer abstractions which are cheaper and access to low-level programming which allows it to have much different performance profile.

NativeAOT's primary goal is reducing memory footprint, binary size, making "run many methods once or rarely" much faster (CLI and GUI applications, serverless functions) and also shipping to targets where JIT is not allowed or undesirable. It can also be used to ship native dynamically or statically (the latter is tricky) linked libraries.

neonsunset··on Programming language speed comparison using Leibniz formula for π
https://github.com/niklas-heer/speed-comparison/blob/9681e8e...
neonsunset··on Programming language speed comparison using Leibniz formula for π
Nim "cheats" in a similar way C and C++ submissions do: -fno-signed-zeros -fno-trapping-math

Although arguably these flags are more reasonable than allowing the use of -march=native.

Also consider the inherent advantage popular languages have: you don't need to break out to a completely niche language, while achieving high performance. Saying this, this microbenchmark is naive and does not showcase realistic bottlenecks applications would face like how well-optimized standard library and popular frameworks are, whether the compiler deals with complexity and abstractions well, whether there are issues with multi-threaded scaling, etc etc. You can tell this by performance of dynamically typed languages - since all data is defined in scope of a single function, the compiler needs to do very little work and can hide the true cost of using something like Lua (LuaJIT).

neonsunset··on Memory Safety in C# vs. Rust
C# actually already has limited lifetime analysis :)

https://em-tg.github.io/csborrow/

> Ownership model for example, would it be possible to enforce practice via some-sort of meta framework?

It should be possible to at least write an analyzer which will be based on IDisposable-ness of types to drive this. Notably, it is not always more efficient to malloc and free versus using GC, and generational moving GCs do not operate on "single" objects allocating and freeing them, no, so you cannot "free" memory either (and it's a good thing - collection is marking of live objects and everything unused can be reclaimed in a single step).

Also the underlying type system and what bytecode allows is quite a bit more powerful than what C# makes use of, so a third language targeting .NET could also yield a better performance baseline by better utilizing existing (very powerful) runtime implementation.

Lastly, there have been many improvements around devirt and object escape analysis, and GC changes are also a moving target (thanks to Satori GC), so .NET is in quite a good spot and many historical problems were or are in the process of being solved, that make Rust-style memory management less necessary (given in Rust you also make use of it because you want to be able to run your code on bare metal or without GC at all, only relying on host-provided allocator - if you do not have such requirement, you have plenty of more convenient options).

neonsunset··on Programming language speed comparison using Leibniz formula for π
Try that on ARM64 and the result will be the opposite :)

On M4 Max, Go takes 0.982s to run while C# (non-SIMD) and F# are ~0.51s. Changing it to be closer to Go makes the performance worse in a similar manner.

neonsunset··on Programming language speed comparison using Leibniz formula for π
> Go fastest language with own compiler (ie not dependent to GCC/LLVM).

C# is using CoreCLR/NativeAOT. Which does not use GCC or LLVM also. Its compiler is more capable than that of Go.

neonsunset··on Patterns for Defensive Programming in Rust
Whether the value is null (and under which conditions) is encoded into the nullability of return value. Unless you work with a project which went out of its way to disable NRTs (which I've sadly seen happen).
neonsunset··on Ask HN: Modern C# book for experienced developers?
Not books but it's really important, in my opinion, to go through official documentation

https://learn.microsoft.com/en-us/dotnet/core/whats-new/

https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/

Can also recommend reading:

https://learn.microsoft.com/en-us/dotnet/core/tools/ (CLI, it's good)

https://learn.microsoft.com/en-us/aspnet/core/fundamentals/m...

https://typescript-is-like-csharp.chrlschn.dev/pages/interme...

> are there books specifically about using .NET Core on Linux

There is nothing particular to using ".NET on Linux" - it just works. Standard Linux caveats not specific to .NET apply.

neonsunset··on Ported freetype, fontconfig, harfbuzz, and graphite to Fil-C
.NET Native is essentially dead. NGEN is specific to .NET Framework. It is possible to dynamically link multiple binaries with the same runtime with NativeAOT but it is very exotic and undocumented path that is not guaranteed to be stable. Not much worse than Go's dynamic linking which also requires the same compiler version.
neonsunset··on A million ways to die from a data race in Go
in .NET, async/await does not protect you from data races and you are exposed to them as much as you are in Go, but there is a critical difference in that data races in .NET can never result (not counting unsafe) in memory safety violations. They can and will in Go.
neonsunset··on Reinventing how .NET builds and ships (again)
Note how practitioners of .NET praise it and non-practitioners (users of .NET Framework) criticize it.
neonsunset··on .NET 10
What papercuts?
neonsunset··on .NET 10
It is as simple as what you get with Cargo, and possibly even more readable.

.NET, unlike Go, has all needed management commands built into its CLI too: dotnet new {template}, dotnet add/remove package, dotnet sln add/remove, etc.

neonsunset··on Why I love OCaml (2023)
Ref structs are hard to solve because they require, in some ways, a lot more work than the way C# solved the lifetimes as the lifetimes in F# may need to become much closer to what they are in Rust. Some constructs may not be able to consume them still (can’t move ref struct into an object with effective lifetime ‘static) and there really is no easy solution short of language editions and breaking standard library changes. A limited improvement is possible, but we’re still in the realm where the lifetimes may become a part of type identity or somehow generalized over, which is hard.
neonsunset··on Why I love OCaml (2023)
> has an edge on some domains due to having unboxed types

If a language makes "unboxed types" a feature, a specific distinction, and has to sell "removing global lock" as something that is a massive breakthrough and not table stakes from 1.0, it can't possibly be compared to F# in favourable light.

neonsunset··on A Note on Fil-C
> C#/f# are safer from that particular problem and more performant than either go or swift, but have suffered from the same deserialization attacks that java does.

They have not in the past 10 years.

neonsunset··on The state of SIMD in Rust in 2025
If anything, the Mono runtime is what prevents heavier (ab)use of SIMD types in the standard library, or at least it causes additional required effort to make it not regress in performance since it's nowhere near as capable as CoreCLR.

You can find the overview of relevant high-level types here: https://learn.microsoft.com/en-us/dotnet/standard/simd

For low-level types and historical context: https://devblogs.microsoft.com/dotnet/dotnet-8-hardware-intr...

neonsunset··on The state of SIMD in Rust in 2025
It is atypical for otherwise mainly high-level languages to have this. Moreover, C# and F# get this through completely independent work runtime libraries and RyuJIT, not by being lazy and having LLVM do everything which is also why Go and Java are so so far behind in this area.
neonsunset··on The state of SIMD in Rust in 2025
You are not "forced" into unsafe APIs with Vector<T>/Vector128/256/512<T>. While it is a nice improvement and helps with achieving completely optimal compiler output, you can use it without unsafe. For example, ZLinq even offers .AsVectorizable LINQ-style API, where you pass lambdas which handle vectors and scalars separately. It the user code cannot go out of bounds and the resulting logic even goes through (inlined later by JIT) delegates, yet still offers a massive speed-up (https://github.com/Cysharp/ZLinq?tab=readme-ov-file#vectoriz...).

Another example, note how these implementations, one in unsafe C# and another in safe F# have almost identical performance: https://benchmarksgame-team.pages.debian.net/benchmarksgame/..., https://benchmarksgame-team.pages.debian.net/benchmarksgame/...

neonsunset··on Pain Points of OCaml
This is outdated.

    let foo = task {
        let! data = getData()
        return data.value
    }
In F#, you interoperate with Task<T> transparently, and there is a number of community libraries to further enhance the experience. It also supports nice combinators like and! out of box.
neonsunset··on A Few Words About Async
Thank you for the article. I noticed the statement

> A second drawback is that async/await has a performance cost. CPU-bound code written with async/await will simply never be as fast or as memory-efficient as the equivalent synchronous code.

If you are interested, .NET is actively improving at this and .NET 11 will ship with "Runtime Async" which replaces explicitly generated state machines with runtime suspension mechanism. It's not """zero-cost""" for now (for example it can block object escape analysis), and the async calling convention is different to sync, but the cost is massively reduced, the calls can be inlined, optimized away, devirtualized and more in the same way standard sync calls can. There will be few drawbacks to using async at that point, save for the syntax noise and poor default habit in .NET to append Async suffix to such methods. In your own code you can write it tersely however.

As for Rust, it also can optimize it quite well, the "call-level overhead" is much less of a problem there, although I have not studied compiler output for async Rust in detail so hopefully someone with more familiarity can weight in.

neonsunset··on Migrating from AWS to Hetzner
However old, .NET Framework was still using a decent JIT compiler with a statically typed language, powerful GC and a fully multi-threaded environment :)

Of course Node could not compete, and the cost had to be paid for each thinly sliced microservice carrying heavy runtime alongside it.

Page 1 of 34Next →