Hacker Newsnew | past | comments | ask | show | jobs | submit | more neonsunset's commentslogin

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.


(author) Thanks, I'll need to read up on this!


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.


I don't think we need to bring languages and frameworks into this. Some of them make things worse, others -- much better.

Furthermore, the microservices craze only made things worse regardless of PL / framework.

IMO we have an entire generation (maybe two) of devs who never self-hosted. That's the main audience of the article and of many of the comments here.

The rest of us who only scoffed at the cloud and microservices were just waiting the world to start coming back to its senses again.


Avalonia would have been a far, far better option.


We evaluated a ton of options, I'm not sure how Avalonia didn't make the list. Thanks for the tip!


Perhaps wasn't available at the time? It has remained relatively little known to teams that lived comfortably within WPF for years, luckily it's changing. .NET's GUI situation is a mess but Avalonia and Uno make it quite saner.


FWIW Tiered Compilation has been enabled on by default since .NET Core 3.1. If the code tries to use refection to mutate static readonly fields and fails, it's the fault of that code.


No, slices in Go are more akin to ArraySegment but with resizing/copy-on-append. It does not have the same `byref` mechanism .NET supports, which can reference arbitrary memory (GC-owned or otherwise) in a unified way as a single (special) pointer type.


This is wrong.

Slices in Go are not restricted to GC memory. They can also point to stack memory (simply slice a stack-allocated array; though this often fails escape analysis and spills onto the heap anyway), global memory, and non-Go memory.

The three things in a slice are the (arbitrary) pointer, the length, and the capacity: https://go.dev/src/runtime/slice.go

Go's GC recognizes internal pointers, so unlike ArraySegment<T>, there's no requirement to point at the beginning of an allocation, nor any need to store an offset (the pointer is simply advanced instead). Go's GC also recognizes off-heap (foreign) pointers, so the ordinary slice type handles them just fine.

The practical differences between a Go slice []T and a .NET Span<T> are only that:

  1. []T has an extra field (capacity), which is only really used by append()
  2. []T itself can spill onto the managed heap without issue (*)
Go 1.17 even made it easy to construct slices around off-heap memory with unsafe.Slice: https://pkg.go.dev/unsafe#Slice

(*): Span<T> is a "ref struct" which restricts it to the stack (see https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...); whereas, []T can be safely stored anywhere *T can


(can't respond directly and don't have the rep to vouch)

> Span bounds are guaranteed to be correct at all times and compiler explicitly trusts this (unless constructed with unsafe), because span is larger than a single pointer, its assignment is not atomic, therefore observing a torn span will lead to buffer overrun, heap corruption, etc. when such access is not synchronized, which would make .NET not memory safe

Indeed, the lack of this restriction is actually a (minor) problem in Go. It is possible to have a torn slice, string, or interface (the three fat pointers) by mutably sharing such a variable across goroutines. This is the only (known) source of memory unsafety in otherwise safe Go, but it is a notable hole: https://research.swtch.com/gorace


Go pointers can point at the stack or inside objects just fine, they are exactly as expressive as C# unsafe pointers (i.e. more expressive than `ref`).

What Go can't do is create a single-element slice out of a variable or pointer to it. But that just means code duplication if you need to cover both cases, not that it's not expressible at all.


> What Go can't do is create a single-element slice out of a variable or pointer to it.

  var x int
  s := unsafe.Slice(&x, 1)
  fmt.Println(&x == &s[0])
  // Output: true


Good catch! That takes care of the unsafe pointer case, but not the safe ref case.

There's no reason for this to be unsafe - you're asking for a 1-element slice, and the compiler knows that the variable is always going to be there as long as the reference exists.

In C#, `Span<T>` has a (safe) constructor from `ref T`.


In C# nothing stops you from doing `var t = Task.Run(() => ExpensiveButSynchronous());` and then `await`ing it later. Not that uncommon for firing off known long operations to have some other threadpool thread deal with it.

Unless you literally mean awaiting non-awaitable type which...just doesn't make sense in any statically typed language?



Don't bother: https://news.ycombinator.com/item?id=43396171

These people could not care less about engaging with the subject, they are here because they feel obliged to engage in a moment of hatred of what they think is an enemy tribe.


Wow what a thread!


I mean… I provided justifications and links. The fact that you choose to disregard all of that is on you.

And the bit where you got angry because I didn't reply quick enough on an internet forum shows that perhaps you need to improve your manners.


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: