HN user

andygocke

29 karma
Posts0
Comments21
View on HN
No posts found.

Yeah but we have codegen bugs in .NET as well. The biggest difference that stood out to me in this write up, is we would have gone straight for “coredump” instead of other investigation tools. Our default mode of investigating memory corruption issues is dumps.

There are two types of initializers: internal and external. Internal are inside the type, like field and property initializers. External are outside, like object initializers, collection initializers, and ‘with’ clauses.

Internal initializers are run as part of the constructor, before any user code. External initializers are run after the constructor, on the constructed object.

For instance:

  class C
  {
    public int P = 5;
  }

  var c = new C { P = 3 };
`c.P` has the value 3.

In your example:

    var n2 = n1.<Clone>$();
    n2.Value = 3;                    // 'with' field setters
    n2.<OnPostCloneInitialise>();  // run the initialisers
The “PostCloneInitializers” you’re running are the field initializers, so the order is backwards. You’re overwriting the value of the external initializers with the internal initializers.

MS have done is to outsource the issue to all C# devs

Let's be clear: breaking dozens of tools because of a change to the IL format also outsources an issue to all C# devs. The .NET IL format has been basically unchanged since .NET 2.0 and huge numbers of people take very hard dependencies on the exact things they do and do not expect. I don't expect we would have been able to make significant changes due to the breaking change impact.

A possible approach I mentioned

This would likely be even harder to understand. For better or worse, the .NET design is that external initializers happen _after_ the constructor runs. That's been true all the way back to when the initializer syntax was first introduced in C# 3. Making regular initializers and `with` initializers have inverted order strikes me as being way worse.

If I could go back in time, I think the main change to C# I would make would be to enforce that the constructor always runs after all external initialization.

Unfortunately, there are alternatives to this behavior, but they all have other downsides. The biggest constraint was the schedule didn't support a new version of the .NET IL format (and reving the IL format is an expensive change for compat purposes, as well). There were two strong lowering contenders, with their own problems.

The first is to use a `With` method and rely on "optional" parameters in some sense. When you write `with { x = 3 }` you're basically writing a `.With(x: 3)` call, and `With` presumably calls the constructor with the appropriate values. The problem here is that optional parameters are also kind of fake. The .NET IL format doesn't have a notion of optional parameters -- the C# compiler just fills in the parameters when lowering the call. So that means that adding a new field to a record would require adding a new parameter. But adding a new parameter means that you've broken binary backwards compatibility. One of the goals of records was to make these kinds of "simple" data updates possible, instead of the current situation with classes where they can be very challenging.

The second option is a `With` method for every field. A single `with { }` call turns into N `WithX(3).WithY(5)` for each field being set. The problem with that is that it is a lot of dead assignments that need to be unwound by the JIT. We didn't see that happening reliably, which was pretty concerning because it would also result in a lot of allocation garbage.

So basically, this was a narrow decision that fit into the space we had. If I had the chance, I would completely rework dotnet/C# initialization for a reboot of the language.

One thing I proposed, but was not accepted, was to make records much more simple across the board. By forbidding a lot of the complex constructs, the footguns are also avoided. But that was seen as too limiting. Reading between the lines, I bet Jon wouldn’t have liked this either, as some of the fancy things he’s doing may not have been possible.

Hi, I own the Native AOT compiler and self-contained compiler for .NET.

Self-contained will work fine because we precompile the runtime and libraries for all supported platforms.

Native AOT won't, because we rely on the system linker and native libraries. This is the same situation as for C++ and Rust. Unlike Go, which doesn't use anything from the system, we try to support interop with system libraries directly, and in particular rely on the system crypto libraries by default.

Unfortunately, the consequence of relying on system libraries is that you actually have to have a copy of the system libraries to link against them, and a linker that supports that. In practice, clang is actually a fine cross-linker for all these platforms, but acquiring the system libraries is an issue. None of the major OSes provide libraries in a way that would be easy to acquire and deliver to clang, and we don't want to get into the business of building and redistributing the libcs for all platforms (and then be responsible for bugs etc).

Note that if you use cgo and call any C code from Go you will end up in the same situation even for Go -- because then you need a copy of the target system libc and a suitable system linker.

I guess people have different experiences as I don’t see the React changes as improving the mobile experience: just the opposite. I often interact with GitHub by browsing through the file tree and various links. But the new react renderer breaks the back button. So often when I’m browsing and hit back, I leave GitHub entirely instead of going back to the parent directory.

First, primitive operations for crypto are intrinsics in Java and operate without FFI at all.

This is a pretty strange assertion given that I didn’t specify the crypto operation I wanted to perform. Is XAES-256-GCM available in the Java standard library?

Doing it this way is not so common in Java anyway

Sure, because doing it the other way would be very expensive. But that doesn’t mean applications which can’t front or backload native processing don’t exist, it just means they will have slower throughput in Java.

It’s fine for a language to make that tradeoff, but it is a tradeoff

Yes, we considered that and implemented a solution. Effectively, the runtime will generate thunk methods that will invisibly bridge between the two worlds. Calling (and overriding) regular async methods with runtime async methods will be stitched up by the runtime. The user will never see the difference.

I agree with your general point, that it depends on your specific problem how difficult this is, but I disagree about how common or easy to work around.

Regarding

But the FFM API does not directly expose Java objects to native code at all, although it does allow Java code to access and mutate "off-heap" native memory (C data) from Java code as efficiently as accessing and mutating Java heap memory

I just don’t buy it. First, I think it’s very common to want to expose managed memory to native. In fact, it might be the dominant case. If I want to call out to perform a crypto operation on a block of bytes I got from a Java operation, I don’t want to copy them first.

Second, I think you’re missing the use case for manipulating system APIs. If you want to perform some system call and the call requires setting up some structures as arguments, that’s going to be pretty expensive in Java. For things that are called a lot it can add up. For example, windows has a profiling and eventing system called ETW. To use it you create a set of events and call the system. It’s not uncommon to do this for thousands or millions of events per second. The way C# handles this is stack allocating an event blob and calling directly. I can’t imagine a Java workaround that would be as fast or simple. It seems like you’d have to pool a native event blob allocation and fill it in from Java.

It’s true that most Java programmers aren’t blocked by this but I think that’s because many Java programmers don’t try to use Java for these tasks. They don’t write systems software in Java and they don’t embed into big, performance-sensitive native apps, like games.

The Gleam example has all the convenience and readability of its C#/Python counterpart - but without the downsides.

This was mentioned in the write-up, but the big downside is interop. Green threads have significant downside when going across OS threads.

This is the same reason why Rust ended up with async. Async is basically the cost you pay for C interop. However, C# runtime-async will likely be much simpler than Rust async since ownership is GC-managed and doesn't need to be transferred across threads.

All that said, I'm also not convinced the codebase bifurcation is a bad thing. Async ~= I/O. As a regular C# user, I'm not particularly unhappy about splitting my app into "I/O things" and "not I/O" things.

In practice we don't think there will end up being tradeoffs in async2 vs. async1. If you look below, at the "JIT state machine" section, you'll see that async2 looked better and the GC behavior differences were probably transient.

Overall, there are no architectural reasons why the compiler version should be better. The runtime should be able to make perf decisions that are at least as good in every case.

I'm not sure that the original description is precisely correct, but yours isn't correct either.

Basically, you can't treat green threads just like "a multi-threaded runtime" and have it just work. That is, a 1:1 mapping between green threads and OS threads is just OS threads.

So fundamentally if you bounce your green stacks off of the actual stack they're going to need to go somewhere... and that place must be the heap.

There are pluses and minuses to this implementation, but the biggest minus is that it makes FFI very complicated. C# has an extremely rich native-interop history (having historically been used to integrate closely with Windows C++ applications) and therefore this approach raised some serious challenges.

In some sense, async is the cost for clean interop with the C/system ABI. Transition across OS threads requires something like async.

I’m the author of the original issue — I agree, we’ll have to ensure the struct layout is solved. I think the only thing that makes sense is to just waste a little space and store the fields side-by-side. In almost all cases where people would use a struct I think this is an acceptable tradeoff.

At the point where you have more than 5 cases, the GC overhead starts to get shrink in comparison to the calling convention and copying overhead anyway.

You're right -- there are a lot of tradeoffs here, and one solution isn't necessarily better.

What Native AOT offers is very small binaries, with very fast startup, and smaller working set. It can also be integrated as a standalone static/shared library into a different application.

Self-contained apps don't require AOT compatibility and may have higher peak throughput (as you said, JITing can be faster in some cases).

It's up to you and your application whether the trade-offs make sense.

.NET 8 will support macOS, and yeah I think your approach of basically re-implementing much of the SDK logic in bazel is probably your best bet for a reliable experience.

At the same time, it's obvious that that's a considerable chunk of work.

Also, IDEs like VS and VS Code are reliant on MSBuild APIs to construct a semantic understanding of the projects, so without MSBuild files, they won't properly function.

This is an unfortunate problem, which I don't have a simple solution for.

Separate thought: .net AOT is still very much experimental. All sorts of things are silently incompatible with it, either in the "refuses to build" or the "crash at runtime" way. I'd only use it for small projects built from the ground up to be AOT.

It's certainly true that most things aren't AOT-compatible out of the box -- this is basically due to the heavy ecosystem reliance on reflection.

BUT, it definitely shouldn't be silently incompatible. We've spent a lot of time building "trimming warnings" (https://learn.microsoft.com/en-us/dotnet/core/deploying/trim...) that should flag any and all incompatibilities.

If there's some incompatibility, a warning should be generated. If there isn't that's likely a bug on us (https://github.com/dotnet/runtime). But conversely, if there are no warnings, you should be guaranteed that the app should be AOT-compatible.

I couldn't find any compile-to-obj command with flags and such, instead there's a `dotnet` command that takes its input via a monolithic `.csproj` XML file and spits out a fully-formed binary

FYI I'm the lead of the Native AOT team at MSFT.

The key thing about .NET AOT is that it compiles the whole framework together, so right now the `dotnet` command is the top-level tool that connects all the pieces together. If you wanted to integrate with other native code, you can use `dotnet` to output a static library, which could then be linked with the rest of your native code.

You're right that this can be complicated to integrate with other build tooling. It's something we can work on, but there's a bit of inherent friction around the .NET side, where it needs to find all the appropriate libraries, NuGet packages, etc, and a make/bazel world.

Definitely something we can work on as we go forward.

I don't think this works... It looks like FetchPersons is free in the body of the receive, and I can't see any indication of the synchronization points in the actor. I mean, maybe every function in the actor is implicitly synchronized, but that's... a choice, and I think it needs other changes to work around that fact.

I actually tried to get Chatgpt to help me design a new language feature for C# and it was useless. Basically just gave me existing C#.

Thanks for the feedback! If you have some details and want to post them on https://github.com/dotnet/runtime, we can include them in planning. As you might have guessed, this is a pretty huge long-term project and many things basically need to be rebuilt from scratch in order to be supported.

Debugging is one area where the form factor will likely demand different behavior vs. JIT, so it will be an evolving scenario. It would be great to know what you would expect vs. what you saw, and importantly what you expect to be different from a traditional native debugging experience (like in C++).