Go vs. C#: Compiler, Runtime, Type System, Modules
medium.com
medium.com
It certainly makes sense for Go to trade off allocation performance for latency--Go employs value types and stack allocation more frequently/idiomatically than C# and as a consequence it enjoys low, constant-time STW pauses. It would actually be pretty interesting if C# made the same tradeoff--it might make C# use value types more frequently/idiomatically as well. IIRC, this would probably drive less inheritance and more composition since I believe value types can't be inherited. You might end up with C# code that looks more like Go?
> The comparison there was done before against .NET Core 2.1, and I plan to share the update for the most current state (.NET Core 3.1 vs Go 1.13.6) next week. But preliminarily, the distance became even larger:
For starters, "the current state" is Go 1.14.4. Updating one and not the other seems unkind.
Go 1.14 also introduced this change:
"The page allocator is more efficient and incurs significantly less lock contention at high values of GOMAXPROCS. This is most noticeable as lower latency and higher throughput for large allocations being done in parallel and at a high rate."
Whether it improved single threaded burst allocation performance or not, I don't know, but it seems likely that such an invasive change would have affected it one way or another.
I also don't find it very compelling to talk about single threaded burst allocation performance... aren't most allocations usually being done across multiple threads, and not in a very bursty (not gigabytes all at once) manner?
I think that Go also stack allocates things more frequently than C#, which makes allocation throughput generally less interesting.
> Isn’t this per-call “extra” (16 bytes on call stack + two comparisons) a bit too expensive?
No? I'm not sure why you think it is. Most local comparisons are extremely cheap on modern processors. Values on the call stack do not have any contention from other threads. Contention would lead to caches being evicted, and the instruction pipeline would be more likely to be thrown out and restarted because of a branch prediction failure. Local values on the call stack should be really easy for the branch predictor to guess, especially if you're repeatedly calling a function. I wouldn't go so far as to say prediction and pipelining make such comparisons free... but it really almost does, from everything I've seen over the years.
> btw, pay attention to an unusual “prologue” of every call that checks for the potential need for stack expansion. That’s the price Go pays for goroutines — most of other static languages don’t do this extra check per every call.
Again, these kinds of checks are rather inexpensive. The cost of growing the stack is amortized over the lifetime of the goroutine, so unless you are constantly creating very short lived goroutines that need large stacks, there really isn't much cost to this. Years ago, Go used to have split stacks, and those were expensive, especially if a hot path was straddling one of the splits between stacks.
> There is seemingly no way to make e.g. a map to rely on your own equality comparer (sometimes you need this), and honestly, I don’t know how to implement a workaround assuming equality is always structural — e.g. even if you start substituting the keys with your own wrappers, wrappers still can’t override their own equality/hashing, so…
Depending on exactly what you're looking for, there are some easy solutions. You can have the "key" be embedded into the larger struct, or you can have a function that returns a key struct. This is an example of using embedding for this purpose, which I think would be a reasonable solution: https://play.golang.org/p/mkHyLZiAYOa
The problem is if you're hoping to later get the key back and have access to the other fields of the struct. In my opinion, that's an antipattern. You should be storing those fields in the value of the map if you want the map to hold them but not use them as part of the key. I've never once (in any programming language) wanted to store an entire key in the key part of a map, but only use part of the key for comparison purposes.
There's no great quote to pull from the article, but there's a strong implication that Go has to compare every field in order to determine equality... but this simply isn't true. Go only has to do this to determine inequality. If you have two pointers, Go will first check whether the pointers are equal. If they are equal, its job is done. Otherwise, it dives into the fields to see whether those are equal or not. Unless you want "reference equality" to mean that you can just cheaply compare the pointers without falling back to structural equality, but... why would you want to do that? You can certainly hack something together with `unsafe`, but I don't recommend worrying about this until you have a benchmark pointing at a specific problematic comparison. (which I've also never seen) Reference equality in the sense that it doesn't fall back to structural (or some other form of) equality is a gigantic footgun that I've seen trip people up in numerous languages. I would rather not have that as a language feature. A lot of people end up accidentally comparing two different pointers that contain the same structural value, and they can't figure out why the two values aren't being treated as equal.
There are a lot of other statements throughout this article that I find questionable, but I feel like I've already written too long of a comment at this point. Yes, "language I know" is a lot easier to like than "language I don't really know", but the conclusion was even more one-sided than I expected, given the level of effort the author seemed to be putting into the article. The absence of generics and sum types are real issues with Go... and even C# doesn't have sum types either, so that's a real problem there too. Almost everything else mentioned in the "C# features missing in Go" section is just completely unnecessary. More is not always better, especially when it comes to language features.
When I google "c# web server", the first thing that pops up is a tutorial for how to write your own from the ground up with sockets. When I do the same thing with Go, I get a practical tutorial for how to use the standard library web server to build an interactive wiki application.
Go's standard library is a huge component of the language's success.
Being able to make an easy-to-deploy binary just by typing "go build" is huge, and the compile times are great. Sure, .NET supports some form of AOT compilation, but it's not the default, and when I google ".net core aot", the first thing that pops up is the CoreRT repo that says "This is an experimental project. We have no plans to productize it in its current form" which is not super encouraging.
There are a lot of things to like about Go, but it is different from many other popular languages, so it's easy for it to make people feel uncomfortable. I'm eager for Go to have generics, because that really is my biggest complaint about it.
If you want a cohesive, awesome type system, Rust is built on a decade or two of research that didn't exist when C# was designed, so C# has to live with a lot of baggage as it expands to include other, newer ideas. Go has avoided most baggage simply by avoiding most features, for better or worse.
While not the same thing as AOT compilation, `dotnet publish -p:PublishSingleFile=true` achieves the goal of having a self contained executable.
Not a critical feature these days with CI being as good as it is, but can you run that command from macOS or Windows and get a Linux binary?
Go ACTUALLY produces single file executables.
The severity of this is up for debate, but I think Microsoft shouldn't be calling this a single file executable.
Source: https://docs.microsoft.com/en-us/dotnet/core/whats-new/dotne...
It -is- a bit of a shame, yes. But 'not productized' isn't quite the same as non functional. Streets of Rage 4 actually uses it for some environments.
The problem is that it seems Microsoft more wanted CoreRT as a research/testbed project for UWP compilation. The community is still trying to get Microsoft to productive it.
> When I google "c# web server", the first thing that pops up is a tutorial for how to write your own from the ground up with sockets. When I do the same thing with Go, I get a practical tutorial for how to use the standard library web server to build an interactive wiki application.
Not that it's a great defense but I checked, and you are right about the first link. the second link, while still a little deeper than one may like, hopefully is enough to realize that ASP.NET is the library to use and puts you just a click or two away from the proper tutorials. I.e. I'm not sure we should be dinging a language based on SEO.
I completely agree that one google search is not enough to ding a language. I’m trying to indirectly make a larger point there, which is that of approachability, especially for people who haven’t been doing all of this for forever. I didn’t want to make that comment significantly longer than it already was, so I’ll expand on that idea in this comment.
> the second link, while still a little deeper than one may like, hopefully is enough to realize that ASP.NET is the library to use and puts you just a click or two away from the proper tutorials.
The second link (for me, at least) appears to be a blog post comparing Kestrel vs HTTP.sys within ASP.NET Core, which wouldn’t be very enlightening to someone who isn’t already familiar with the term “ASP.NET” and how that is basically the standard .NET web framework.
Literally none of the links on the first page for that ill-fated (and arguably slightly wrong) search query, as far as I can tell, are a tutorial that involves serving a web page without writing your own socket code.
From experience trying to help a friend transition from Python to C#, a lot of tutorials I expected to find either weren’t at the right level for a beginner, were way out of date, or didn’t exist at all.
With how rapidly C# and the surrounding tooling and frameworks have been evolving over the last 5 to 10 years (.NET Framework to .NET Core, ASP.NET 4 to ASP.NET Core, etc.), my experience is that there’s a lot of so called “inside baseball” in the C# world, partially due to how out of date a lot of the materials that Google is likely to surface are.
I’ve used C# enough over the years that I don’t think I would have much of a problem finding what I needed if I chose C# for a project, but the language itself has a large amount of complexity, and developers have to make a lot of choices. They have to first figure out .NET Framework versus .NET Core, then they need to figure out that ASP.NET is the very intuitively named web framework that most people use. Also, do I need HTTP.sys or Kestrel? I really don’t know without doing some research, but luckily there’s an article diving right into that topic on link #2 on Google.
To give an even more concrete example, the official ASP.NET tutorial on creating a Web API[0] involves a ton of autogenerated code that is either generated through clicking around all over Visual Studio, or running a bunch of `dotnet` commands on the command line. All of that autogenerated code is brushed over as if the reader must already be an expert in the world of C# but has just somehow never used ASP.NET before. (That seems like an unlikely combination to me.) Even if you’re used to other statically typed languages, you see this exciting code[1]:
// POST: api/TodoItems
[HttpPost]
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
{
_context.TodoItems.Add(todoItem);
await _context.SaveChangesAsync();
//return CreatedAtAction("GetTodoItem", new { id = todoItem.Id }, todoItem);
return CreatedAtAction(nameof(GetTodoItem), new { id = todoItem.Id }, todoItem);
}
First of all, what is a Task? What is an ActionResult? Where did `_context` come from and why does it start with an underscore? The tutorial literally does not even attempt to address the magical appearance of `_context`, or its ability to hold TodoItems. Ok, thinking for just a second, I remember we added a database context, but how would I know it’s going to be called `_context`? Registering our TodoContext object also felt super magical. We just had to “know” to put it in ConfigureServices, and does it matter that the tutorial puts it before `services.AddControllers`? Does the `//POST: api/TodoItems` line actually control anything, or is it just documentation for us? Why do we want to “Replace the return statement in the PostTodoItem to use the nameof operator” anyways? The code seems to do the same thing either way. (I know most of the answers, but these are the questions I would expect someone else to ask as a beginner being confronted with this tutorial.)I think approachability is not currently where I would want it to be with C#.
Compare that tutorial to this Go tutorial.[2] It walks the reader gently through the entire process with only an assumption that the reader has some programming experience, a little knowledge of HTML and HTTP, and the know-how to run a few commands on the command line. The tutorial explains most of the Go-specific stuff you encounter, and it iteratively builds up the project code from scratch, rather than just telling you to “insert a line here in this autogenerated file”, and “swap this one random piece of an already functional piece of code with another piece of code that yields exactly the same result.”
Go “includes the batteries” with its standard library that gives you easy defaults for many things, and the standard library docs often give you great examples for many situations. Goroutines make it so that you just write normal synchronous code, but it still scales extremely well. Tutorials are plentiful, and rarely feel out of date. There are no magically-appearing, unexplained variables like `_context`, and there isn’t a ton of scaffolding that you just have to figure out. All of this makes Go feel like a very approachable language to me.
Is approachability super important to everyone? No, and that’s fine. There are lots of C# developers out there already, and there are lots of people who will figure it all out on their own anyways.
I want to point out that I really do like C# and .NET Core. I would personally much rather use C# than Java, although I tend to think Kotlin is better than either of those. The last time I used C#, it had really great developer tooling, and it really did get a lot of things right. I think I would also prefer to use use C# with ASP.NET Core rather than Ruby on Rails or most Python frameworks these days. I’ve just had a lot of great experiences with Go (and Rust too!) in professional projects over the last several years, and those experiences make it hard for me to want to reach for anything else these days, C# included.
[0]: https://docs.microsoft.com/en-us/aspnet/core/tutorials/first...
[1]: https://docs.microsoft.com/en-us/aspnet/core/tutorials/first...
I might be missing something, but Go definitely doesn't compare every field in order to determine equality or inequality. Go only compares pointers, and it does this because (1) pointers are values and they get compared like values and (2) sometimes you want to do identity comparison and sometimes you want to do value comparison and the language has no way to infer which you intend unless you tell it (and the mechanism for communicating this to the compiler is by the type of of the operands you pass to the equality operator--pointers for identity comparison and values for value comparison).
Here's a little playground link to illustrate: https://play.golang.org/p/zrtTLB6Y9w3
I'm not sure how I got confused with the special case around string comparisons, and forgot that it didn't apply to regular struct pointers. It was late in the evening, so I guess that's my excuse?
BenchmarkSameRef-12 620150179 1.92 ns/op
BenchmarkDiffRef-12 6102709 204 ns/op
Source: https://play.golang.org/p/DbjkstOCZ-Y (although you can't actually run it on the Go playground)Go will check whether two strings have the same underlying pointer first. If the pointers are different, then it compares the contents.
I stand by my earlier statement that I think pure reference equality is just a bad idea 99.9% of the time. I would prefer if Go did deep equality with every comparison when needed the way it does with strings. If you have to use some clunky syntax or built-in function to do just a reference comparison, that would be perfectly fine with me. Unfortunately, Go does exactly what you described.
With a quick google, I didn't immediately find a linter to complain about comparing struct pointers rather than struct values, so maybe I'll write a linter like that sometime if I can find the time and motivation.