HN user

neonsunset

2,773 karma

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

Posts25
Comments2,440
View on HN
fstandsfor.fun 1y ago

Up and Running with F#

neonsunset
1pts0
www.microsoft.com 1y ago

RE#: High Performance Derivative-Based Regex Matching [pdf]

neonsunset
2pts0
typesanitizer.com 1y ago

Experience Report: 6 months of Go (2022)

neonsunset
2pts0
github.com 1y ago

FSharpPacker – compile F# scripts to executables

neonsunset
88pts8
hez2010.github.io 1y ago

How much memory do you need in 2024 to run 1M concurrent tasks?

neonsunset
264pts196
learn.microsoft.com 1y ago

What's New in F# 9

neonsunset
283pts177
twitter.com 1y ago

Godot social media account blocks titanium level backer for the Godot fund

neonsunset
51pts13
swharden.com 1y ago

Curve Fitting in C# Using Particle Swarm Optimization

neonsunset
1pts0
www.youtube.com 1y ago

What's new in F# 9 – Languages and Runtime Community Standup [video]

neonsunset
3pts0
deadlockempire.github.io 2y ago

The Deadlock Empire: An Interactive Guide to Locks

neonsunset
216pts41
lemire.me 2y ago

Scan HTML faster with SIMD instructions: .NET/C# Edition

neonsunset
41pts4
diffsharp.github.io 2y ago

Diffsharp: A Tensor Framework for F#

neonsunset
3pts0
mail.openjdk.org 2y ago

Java string interpolation feature has been cancelled

neonsunset
71pts114
github.com 2y ago

Hopac: Concurrent ML Implementation in F#

neonsunset
18pts0
fractalfir.github.io 2y ago

Rust to .NET compiler: Statically Sized, dynamically sized, and other

neonsunset
4pts1
github.com 2y ago

ILGPU: Write GPU programs with C# and F#

neonsunset
157pts35
www.youtube.com 2y ago

Deep Dive into .NET's Regex Engine with Stephen Toub [video]

neonsunset
2pts0
github.com 2y ago

Rust for Fsharpers and F# for Rustaceans

neonsunset
5pts1
github.com 2y ago

Pinta – A Simple GTK 4 Paint Program (C#)

neonsunset
1pts0
www.postgresql.org 2y ago

Announcing pl/dotnet, version 0.99 (beta) for PostgreSQL

neonsunset
2pts0
github.com 2y ago

Gir.Core – a C# library for writing GTK(GObject)-based applications

neonsunset
1pts0
www.youtube.com 2y ago

Writing async/await from scratch in C# with Stephen Toub [video]

neonsunset
36pts0
github.com 2y ago

Sourcegit: A Cross-Platform Git Client Built with Avalonia and C#

neonsunset
2pts1
github.com 2y ago

C# Regex engine is now 3rd fastest in the world

neonsunset
5pts2
github.com 4y ago

FastCache.Cached: high-performance lock-free caching library in .NET

neonsunset
2pts0

.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++.

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.

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).

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");
    }

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!

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.

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.

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).

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).

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).

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.

.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.

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.

.NET 10 8 months ago

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.

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.

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.

A Note on Fil-C 9 months ago

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.

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...

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.

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/...

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.

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.

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.