24 ms·
Unity has been using C# for low-level code for years, and they have even gone as far as compile C# code into shaders. It's surprisingly flexible, and unsafe low
by t8sr 5y ago
Unity has been using C# for low-level code for years, and they have even gone as far as compile C# code into shaders. It's surprisingly flexible, and unsafe low-level code composes well with high-level code quite well.
You can really tell, though, that the language wasn't meant to be used this way. Some common annoyances are:
1) C# is unable to declare fixed-size arrays in structs. There are ways around it, but they are all more unsafe, require code generation or hit the GC. If you've ever written kernel code, you know that fixed-size data members are really common, so this is more annoying that you might think.
2) C# types are either always references (classes) or always values (everything else). There are reference types, but they are restricted so that, for example, the array subscription operator cannot return them. This means that you can't directly operate on values in an array. Once again, there are ways around this, but they involve pointers, which pull in a bunch of the "unsafe" language.
3) The standard library makes a lot of choices that, in hindsight, are extremely unfortunate. Data structures carry generation numbers that increment on various operations and invalidate iterators. This makes a lot of operations that are sublinear in C++ take linear time in C#. You pretty much can't use the C# stdlib for any low-level code.
The projects that successfully use C# in low-level code pretty much all roll their own containers, stdlib and a module that implements black-magic stuff, usually called something like UnsafeUtil. On balance, having written a bunch of lower-level C# code, I'd still slightly prefer it to C++ for (e.g.) talking to the GPU driver, but there's no denying that that's a very low bar to clear. For general kernel code I wouldn't use it.
The 4 languages I would consider for low-level code these days are, in order:
- C/C++ (footguns galore, but lots of existing literature)
- Go (surprisingly easy to control the GC and get it to do what you want)
- Rust (the Rick & Morty of programming languages - not a compliment)
- C# (See above)
- fsloth 5y agoCould you expand on this a bit: "- Rust (the Rick & Morty of programming languages - not a compliment)"? Rick&Morty being a very long going (and excellent) parody version of Back To The Future franchise I'm not sure what qualities this characterization imbues on a programming language and would like to hear your views on the matter (since I found your analysis of C# very much agreeable).
- t8sr 5y agoHonestly, I don’t feel qualified to evaluate Rust. At face value, it seems like a good replacement for C++, but I’ve never successfully used it for anything larger than a toy. That’s probably mostly on me. My comment was referring to the impression that people who like Rust seem to think that you either also accept Rust as the best thing ever, or you don’t get it. I probably shouldn’t have mentioned it, because now the thread will be about Rust.
- arethuza 5y ago"people who like Rust seem to think that you either also accept Rust as the best thing ever, or you don’t get it" I don't think Rust is in any way unique in having people who think that way...
- martincmartin 5y ago"There are only two kinds of languages: the ones people complain about and the ones nobody uses." -- Bjarne Stroustrup https://www.stroustrup.com/quotes.html#:~:text=%22There%20are%20only%20two%20kinds,and%20the%20ones%20nobody%20uses%22 https://www.stroustrup.com/quotes.html#:~:text=%22There%20ar....
- chakkepolja 5y agoIt has got so many people from ruby and js crowds so there will be statistically more of them.
- t8sr 5y agoProbably not, but it's a question of degree. The projects I work on don't get monthly requests to drop everything and rewrite them in Haskell/Clojure/Go but we do get them for Rust. An anecdote: I recently opened a position on my team, and got two applicants on the same day who ignored the project description, and basically said we should hire them to tell us how everything we do could be better if we rewrote multiple projects in Rust. Of course it's completely possible for a programming language to be both (A) really well-designed and (B) have annoying people among its community. I suspect that's the case here.
- kevingadd 5y agoSome of this isn't true anymore. For example, #2 is wrong: You've always been able to get a reference to an array element (both at an IL level, and at a C# level). It used to be somewhat awkward - you would need to pass 'ref arr[x]' to a function - but you can store references in locals now. Agreed on #3, a lot of the BCL is just miserable in terms of design. There have been improvements lately but I'm still stuck using my own custom containers for various cases. One good move the BCL and runtime teams have made over time is to just expose more unsafe primitives so you can easily build your own stuff - the Memory and Unsafe classes give you lots of useful tools, Span<T> and Memory<T> let you safely work with memory allocated in various ways, and various BCL stuff has been updated to accept Spans instead of requiring you to use managed arrays.
- t8sr 5y agoThanks, I didn’t know about the expanded use of refs. As the other commenter pointed out, the version of C# used in Unity is out of date by a version or two, and so is my experience.
- ygra 5y agoConsidering we're close to C# 10 by now, it's ... a few more versions ;-) Hard to blame Unity here, though. They have written their own compiler backend that converts CIL to C++ and that was back in the .NET Framework days when performance wasn't that high a priority for language and framework design.
- kevingadd 5y agoTheir transpiler would probably be up to date if they open-sourced it, since plenty of other people would be using it. Instead people are stuck using CoreCLR's AOT backend or other closed transpilers like BRUTE. Their transpiler also had a lot in common with ones for other targets (I know for a fact that in the past it used to be very similar structurally to JSIL) which probably improves the likelihood that they would have been able to leverage community knowledge and contributions here. It's understandable though that they want their transpiler and C#->shader compiler to be their secret sauce, though, since they see them as unique advantages over Godot, Unreal, etc.
- bodge5000 5y agoWhy is Rust the Rick and Morty of programming languages? I've wanted to learn it for ages (mostly because of bevy, but it looks nice itself), but lately I've been hearing not so good things about it
- BoxOfRain 5y agoLike the TV series, some of its "fanbase" are a bit obnoxious and self-important. They're a bit like people who'll fight a Middle Ages worth of religious crusades over vi/emacs vs graphical IDEs or *nix vs Windows rather than just using what works best for a specific scenario. The language itself is really cool, I'm a big fan even if I've not had many chances to write it professionally. It's a really well thought out language, I wouldn't use it in every single scenario but there's a lot of legitimate use-cases for it (especially if you need lots of performance but don't fancy the footguns of C++). The learning curve is quite steep but it's nowhere near as bad as some people make it out to be.
- bodge5000 5y agoI guess every language comes with its negative connotations. I've heard languages like python and lua referred to as "baby's first language" because they're compartively easy to start out with, C and C++ being for the kind of people who like to talk about how they use it more than they actually use it, the list goes on. And of course all of them will fight over the specifics (in the case of python, I've been guilty of this when it comes to tabs vs spaces).
- BoxOfRain 5y agoI agree although having said I think there's one exception to this rule which is Kotlin, I use it a lot but I've never heard of a dedicated Kotlin hater or evangelist.
- frenchy 5y agoThe difference is that normally when you make fun of a language like that, it's mostly a joke. You don't normally use it to completely dismiss a whole argument that could actually have real substance. The only other langauages I see people this with, are VB & PHP (for somewhat different reasons).
- AQuantized 5y agoI used to love C but it's so hard to accept the possibility I'm going to compile something totally unsafe after using Rust. Do you think Rust's ecosystem isn't going to amount to anything, or that it's just a meme, or what do you intend with the Rick and Morty analogy?
- oscargrouch 5y agoI think Rust kind of aged C. It happens when some new band push the needle for a genre, its only when this happens that the "modern" stuff start to look old. C is getting deprecated while C++ will need to catch up somehow (if there's a way). This is actually a good effect of Rust, making people think more that safety is important and that its possible to have more of that in languages without a GC. (Sorry for hijacking the question, that was not directed at me, but this is a good debate so i couldn't resist)
- pjmlp 5y agoMany of those caveats have been improved since C# 7, blame Unity for not keeping up with .NET versions. https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-7#enabling-more-efficient-safe-code https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csh... https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-8#disposable-ref-structs https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csh... https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-8#unmanaged-constructed-types https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csh... https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-8#stackalloc-in-nested-expressions https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csh... https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-9#performance-and-interop https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csh...
- t8sr 5y agoOh, thank you for that. I’m really happy to hear there’s been movement on this front. Hopefully Unity will get there eventually - I suspect their own solutions to some of these problems might be holding back adoption of more recent .Net versions, at this point.
- runevault 5y agoThey claim an upcoming project is to upgrade to .NET 5 (though at this rate I would more expect 6).
- novaleaf 5y agoyes, just as you imply, don't hold your breath ;)
- da_chicken 5y agoI imagine they got tied up in all the .Net changes between 2016 and now. I wouldn't be surprised if they were just waiting for Microsoft to pick a path long enough to have a target. .Net 6 of makes sense because it's supposed to be an LTS version. However, if the release and EOL projections[0] I don't think it actually matters all that much: "LTS releases get free support and patches for 3 years. Current releases get free support and patches for 18 months." That feels fairly aggressive for a library and language for business applications. I imagine many projects won't be released until the version of .Net used to build it is EOL. [0]: https://dotnet.microsoft.com/platform/support/policy/dotnet-core https://dotnet.microsoft.com/platform/support/policy/dotnet-...
- MaXtreeM 5y agoI think all of the issues in #2 might have been resolved with changes to ref keyword and ref-structs added, both in C# 7.
- hootbootscoot 5y agono Zig nor Nim nor D but you include 2 GC'ed languages as suitable for "low-level code"???
- pjmlp 5y agoBecause they have been proven in the battlefield? https://www.f-secure.com/en/consulting/foundry/usb-armory https://www.f-secure.com/en/consulting/foundry/usb-armory https://tinygo.org https://tinygo.org https://www.wildernesslabs.co/ https://www.wildernesslabs.co/
- t8sr 5y agoYou know, it's interesting to me how much people fixate on garbage collection. Taking Go as example, there are many pitfalls to using it in low-level situations, but the GC is, in practice, never a source of significant issues. The real problems are: 1) The foreign function interface (CGO) requires a context switch in both directions. (Not because of GC, but because of Go's threading model.) A lot of work ends up being in trying to avoid calls between Go and C in hot loops. 2) Especially talking to the kernel, there are surprising sharp edges in the ABI and C calling convention. I once spent a week trying to populate a bit field correctly. (But other languages would have the same problems matching any C behavior that's left up to the compiler.) 3) The lack of memory fences is a big deal when trying to use kernel-managed ring buffers. The GC, comparatively, is problem-free for two reasons: 1) A lot of embedded code uses static buffers anyway. 2) The problem of managing memory doesn't go away just because you're in ring0. The Linux VFS, for example, is heavily garbage collected, it just uses a jerry-rigged GC consisting of counting references. I'm sure if I make a categorial statement on this someone on HN will take me to school, but I think a real GC might be a net performance win in some places in the Kernel, like the VFS. Finally, people often bring up that GC is fine for throughput, but not for latency. Fair enough, but manual reference counting is also not better: someone has to call free at some point, and if you happen to draw the short straw in a blocking system call, it still affects latency. The amortization behavior is probably different, but not in any way that I think matters to most people.
- mumblemumble 5y ago
- deleted 5y ago[deleted]
- mdip 5y agoEchoed my thoughts pretty closely here. My thinking has always been "it's a language and a runtime". I found your bullet points interesting and realized it's been a really long time since I've done any serious systems programming... 1) You mention there's a workaround -- I can't recall exactly what it is but I think an attribute added to the array can be used to force this behavior -- that's a language limitation, for sure, in that there's effectively no way using language syntax to declare that (and no way for the compiler to verify that) 2) The latest version of the language blurs the lines here in a few ways -- using ref types allows one to "directly operate on values in an array" without resorting to unsafe. I think there's really two things to consider in these cases: (1) Can C# handle the requirement at all? (2) Can C# handle it in a way that is compiler verified, or runtime verified without the use of `unsafe`? The answer to the first question, in my years of working with the language, have always been "Yes". Ultimately, I can find an attribute or fall back to unsafe and do what I need to do without the compiler's help, but the frequency with which I've needed to do so has been "very close to never"[0]. The answer to the second question is often "Yes", too, especially in the most recent versions. The few cases where I have had to drop to `unsafe` for a performance critical piece of code, I have been able to replace with ReadOnlySpan/Span in every case I've encountered, for example. [0] But I don't write systems software in C#
- defaultname 5y agoI absolutely adore Go, finding it to be an imperfect language that nonetheless yields powerful, maintainable solutions. It is the ultimate duct-tape language, allowing one to build robust, performant systems using a massive library and fantastic C interop where necessary. Yet I wouldn't consider it for "low-level" code. Garbage collection alone invalidates it in that regards (religious war: reference counting beats GC every time), in the same way that C# and Java aren't credible low level languages. You aren't going to see C# or Go used in the Linux kernel. "Rust (the Rick & Morty of programming languages - not a compliment)" You mean something that is good to great but that "outsiders" or late adopters feel unexplainably threatened by and thus feel the need to attack it and its admirers? Where ironically the detractors are louder and more numerous than the advocates ever were? Rust is particularly susceptible to this effect as it has been racking up wins and growing in influence, making people feel insecure about their current skillset/knowledge (always a weird motivator for rhetoric in this industry). I have only dabbled in Rust a few times. It feels very unintuitive and "write-only" from my 1000 foot, ignorant perspective. But I realize that's on me, and appreciate the focus and purpose of the language. Rust today seems like a 2013 Tesla Model S. A couple of years ago it was the GM EV1: It used to be largely promise of a future to come with a loads of downsides, where today it's expensive to get started with, has a lot of incompatibilities, but it's superior to most alternatives. Tomorrow it and those like it will be the norm.
- isubasinghe 5y agoRust is bloody great, the type system is so good. My thesis code has a bunch of type conversions, I managed to emulate 'do' notation (Haskell) of sorts via the type system, but I can now also convert between types really easily. So bloody happy with it. for example `n.unbind().bind()` can convert to a different type but `inside_n = n.unbind()` will let you work with the data inside the container `n` and after you are finished you can call `inside_n.bind()` to lift it back up to the container type.
- pjmlp 5y ago> You aren't going to see C# or Go used in the Linux kernel. Nor C++ or Ada, so what? Linux isn't some kind of golden rule for all OSes on the galaxy. I am more than happy to do bare metal coding in C# (Meadow) and Go (TinyGo) without having to deal with Linux.
- nikeee 5y ago> The 4 languages I would consider for low-level code these days are, in order: What about Zig? https://ziglang.org https://ziglang.org
- pjmlp 5y agoFirst the use-after-delete use case needs to be covered, we already have C, C++ and Objective-C for that.
- deleted 5y ago[deleted]
- da_chicken 5y agoI don't expect Zig to take off until A.D. 2101.
- e-master 5y agore #1 - to be fair, even if you have to use `unsafe`, it's not that bad [1]: unsafe struct FixedBufferExample { public fixed int _buffer[1024]; // This is a fixed buffer. } Sure, you need to be careful, but given that you'd be doing systems programming anyway, you probably would prefer to have explicit control over bounds checking, since the compiler/JIT is (unfortunately) not sophisticated enough yet to elide bounds checking in many cases. [1] https://thedeveloperblog.com/fixed-buffer https://thedeveloperblog.com/fixed-buffer
- amir734jj 5y agoCan you please expand on #3, why is it bad for system programing?
- adamrezich 5y agowithout the standard library data structures, what's the point of using C# for low-level programming? kinda seems like a square-peg-round-hole situation.
- bitwize 5y agoLeaving aside that entire operating systems were written in Lisp, I suggest you consider adding Ada to that list. It is very strict, and feels clunky to use by today's standards, but it is highly readable, has a good set of safety features, and an actual spec with a comprehensive conformance test suite. And it's been proven in defense and aeronautical use for decades -- two industries where lives are literally on the line.
- mbrodersen 5y agoOperating Systems has been written in a lot of languages. However the operating systems that people actually use are written in C/C++.
- deleted 5y ago[deleted]
- mastrsushi 5y agoYou can declare array length in a constructor. Otherwise you can just use a list and don’t add past a set capacity.