Go optimizing for latency over throughput means the GC very rarely interferes with my applications SLO at the cost (literally $) of presumably requiring more total compute than a GC that allows more fine tuned tradeoffs between latency and throughput.
As someone who is not directly paying the bills but has wasted far too much of my life staring at JVM GC graphs and carefully tuning knobs, I vastly prefer Go’s opinionated approach. Obviously not a universally optimal choice but I’m so thankful it works for me! I don’t miss pouring over GC docs and blog posts for days trying to save my services P99 from long pauses.
You are comparing very old Java if you had to touch anything else than heap size.
Especially that Java's GCs are by far the very very best, everything else is significantly behind (partially because other platforms may not be as reliant on object allocation, but it depends on your usecase)
With traditional low latency GC designs (e.g. Shenandoah/G1), faster hardware provides almost no benefit because the GC pause is based around the time for core to core communication which hasn't decreased much (since we keep adding extra cores so the fight has to be to keep it from getting slower)
The benchmarks don't necessarily use all of the cores though, and memory bandwidth has increased considerably since those original benchmarks, so you should expect pause times to decrease from that alone.
You can use GC defaults, but tuning can provide valuable throughput or latency improvements for the application if you tune the GC parameters according to the workload. Especially latency sensitive applications may benefit from generational ZGC in modern JVMs.
.NET is not far behind at all. It is also better at heap size efficiency and plays nicely with interop. Plus the fact that the average allocation traffic is much lower in .NET applications on comparable code than in Java also helps.
Other way of putting it is that Golang optimizes for latency over throughput because it would suck at latency if it only optimized for throughput. That can only be called a sensible choice.
Weak point of Golang though is its terrible interop with C and all C-compatible languages. Means you can't optimize parts of a Golang app to dispense with GC altogether, unless using a totally separate toolchain w/ "CGo".
> unless using a totally separate toolchain w/ "CGo".
CGo is built into the primary Go toolchain... it's not a 'totally separate toolchain' at all, unless you're referring to the C compiler used by CGo for the C code... but that's true of every language that isn't C or C++ when it is asked to import and compile some C code. You could also write assembly functions without CGo, and that avoids invoking a C compiler.
> Means you can't optimize parts of a Golang app to dispense with GC altogether
This is also not true... by default, Go stack allocates everything. Things are only moved to the heap when the compiler is unable to prove that they won't escape the current stack context. You can write Go code that doesn't heap allocate at all, and therefore will create no garbage at all. You can pass a flag to the compiler, and it will emit its escape analysis. This is one way you can see whether the code in a function is heap allocating, and if it is, you can figure out why and solve that. 99.99% of the time, no one cares, and it just works. But if you need to "dispense with GC altogether", it is possible.
You can also disable the GC entirely if you want, or just pause it for a critical section. But again... why? When would you need to do this?
Go apps typically don't have much GC pressure in my experience because short-lived values are usually stack allocated by the compiler.
> You can write Go code that doesn't heap allocate at all
In practice this proves to be problematic because there is no guarantee whether escape analysis will in fact do what you want (as in, you can't force it, and you don't control dependencies unless you want to vendor). It is pretty good, but it's very far from being bullet-proof. As a result, Go applications have to resort to sync.Pool.
Go is good at keeping allocation profile at bay, but I found it unable to compete with C# at writing true allocation-free code.
As I mentioned in my comment, you can also observe the escape analysis from the compiler and know whether your code will allocate or not, and you can make adjustments to the code based on the escape analysis. I was making the point that you technically can write allocation-free code, it is just extremely rare for it to matter.
sync.Pool is useful, but it solves a larger class of problems. If you are expected to deal with dynamically sized chunks of work, then you will want to allocate somewhere. sync.Pool gives you a place to reuse those allocations. C# ref structs don't seem to help here, since you can't have a dynamically sized ref struct, AFAIK. So, if you have a piece of code that can operate on N items, and if you need to allocate 2*N bytes of memory as a working set, then you won't be able to avoid allocating somewhere. That's what sync.Pool is for.
Oftentimes, sync.Pool is easier to reach for than restructuring code to be allocation-free, but sync.Pool isn't the only option.
> sync.Pool is useful, but it solves a larger class of problems. If you are expected to deal with dynamically sized chunks of work, then you will want to allocate somewhere. sync.Pool gives you a place to reuse those allocations. C# ref structs don't seem to help here, since you can't have a dynamically sized ref struct, AFAIK. So, if you have a piece of code that can operate on N items, and if you need to allocate 2*N bytes of memory as a working set, then you won't be able to avoid allocating somewhere. That's what sync.Pool is for.
Ref structs (which really are just structs that can hold 'ref T' pointers) are only one feature of the type system among many which put C# in the same performance weight class as C/C++/Rust/Zig. And they do help. Unless significant changes happen to Go, it will remain disadvantaged against C# in writing this kind of code.
Only the whiskers are touching, and the same applies to several other languages too. Yes, the median is impressively low… for anything other than those three. And it is still separate.
C# has impressive performance, but it is categorically separate from those three languages, and it is disingenuous to claim otherwise without some extremely strong evidence to support that claim.
My interpretation is supported not just by the Benchmarks Game, but by all evidence I’ve ever seen up to this point, and I have never once seen anyone make claim that about C# until now… because C# just isn’t in the same league.
> Ref structs (which really are just structs that can hold 'ref T' pointers)
A ref struct can hold a lot more than that. The uniquely defining characteristic of a ref struct is that the compiler guarantees it will not leave the stack, ever. A ref struct can contain a wide variety of different values, not just ref T, but yes, it can also contain other ref T fields.
I’m saying C# as a whole, not C# on one example. But, I have already agreed that C#’s performance has become pretty impressive. I also still believe that idiomatic Rust is going to be faster than idiomatic C#, even if C# now supports really advanced (and non-idiomatic) patterns that let you rewrite chunks of code to be much faster when needed.
It would be interesting to see the box plot updated to include the naot results — I had assumed that it was already.
This is a distribution of submissions. I suggest you look at the actual implementations and how they stack-up performance wise and what kind of patterns each respective language enables. You will quickly find out that this statement is incorrect and they behave rather closely on optimized code. Another good exercise will be to actually use a disassembler for once and see how it goes with writing performant algorithm implementation. It will be apparent that C# for all intents and purposes must be approached quite similarly with practically identical techniques and data structures as the systems programming family of languages and will produce a comparable performance profile.
> No…? https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...
A ref struct can hold a lot more than that. What’s unique about a ref struct is that the compiler guarantees it will not leave the stack, ever. A ref struct can contain all sorts of different stack-allocatable values, not just references.
Do you realize this is not a mutually exclusive statement? Ref structs are just structs which can hold byref pointers aka managed references. This means that, yes, because managed references can only ever be placed on the stack (but not the memory they point to), a similar restriction is placed on ref structs alongside the Rust-like lifetime analysis to enforce memory safety. Beyond this, their semantics are identical to regular structs.
I.e.
> C# ref structs don't seem to help here, since you can't have a dynamically sized ref struct, AFAIK
Your previous reply indicates you did not know the details until reading the documentation just now. This is highly commendable because reading documentation as a skill seems to be in short supply nowadays. However, it misses the point that memory (including dynamic, whatever you mean by this, I presume reallocations?) can originate from anywhere - stackalloc buffers, malloc, inline arrays, regular arrays or virtually any source of memory, which can be wrapped into Span<T>'s or addressed with unsafe byref arithmetics (or pinning and using raw pointers).
Ref structs help with this a lot and enable many data structures which reference arbitrary memory in a generalized way (think writing a tokenizer that wraps a span of chars, much like you would do in C but retaining GC compatibility without the overhead of carrying the full string like in Go).
You can also trivially author fully identical Rust-like e.g. Vec<T>[0] with any memory source, even on top of Jemalloc or Mimalloc (which has excellent pure C# reimplementation[1] fully competitive with the original implementation in C).
None of this is even remotely possible in any other GC-based language.
People have had a long time to submit better C# implementations. You are still providing no meaningful evidence.
> Do you realize this is not a mutually exclusive statement?
It doesn’t have to be mutually exclusive. You didn’t seem to understand why people care about ref structs, since you chose to focus on something that is an incidental property, not the reason that ref structs exist.
Brigading you is not my intent, so please don't take my comments that way.
I just want to add that C# is getting pretty fast, and it's not just because people have had a long time to submit better implementations to a benchmark site.
The language began laying the groundwork for AOT and higher performance in general with the introduction of Span<T> and friends 7 or so years ago. Since then, they have been making strides on a host of fronts to allow programmers the freedom to express most patterns expected of a low level language, including arbitrary int pointers, pointer arithmetic, typed memory regions, and an unsafe subset.
In my day-to-day experience, C# is not as fast as the "big 3" non-GCed languages (C/C++/Rust), especially in traditional application code that might use LINQ, code generation or reflection (which are AOT unfriendly features- i.e. AOT LINQ is interpreted at runtime), but since I don't tend to re-write the same code across multiple languages simultaneously I can't quantify the extent of the current speed differences.
I can say, however, that C# has been moving forward every release, and those benchmarks demonstrate that it is separating itself from the Java/Go tier (and I consider Go to be a notch or two above JITed Java, but no personal experience with GraalVM AOT yet) and it definitely feels close to the C/C++/Rust tier.
It may not ever attain even partial parity on that front, for a whole host of reasons (its reliance on its own compiler infrastructure and not a gcc or llvm based backend is a big one for me), but the language itself has slowly implemented the necessary constructs for safe (and unsafe) arbitrary memory manipulation, including explicit stack & heap allocation, and the skipping of GC, which are sort of the fundamental "costs of admission" for consideration as a high performance systems language.
I don't expect anyone to like or prefer C#, nor do I advocate forcing the language on anyone, and I really hate being such a staunch advocate here on HN (I want to avoid broken record syndrome), but as I have stated many times here, I am a big proponent of programmer ergonomics, and C# really seems to be firing on all cylinders right now (recent core library CVEs notwithstanding).
I just don’t like seeing people make bold claims without supporting evidence… those tend to feel self-aggrandizing and/or like tribalism. It also felt like a bad faith argument, so I stopped responding to that other person when there was nothing positive I could say. If the evidence existed, then they should have provided evidence. I asked for evidence.
I like C#, just as I like Go and Rust. But languages are tools, and I try to evaluate tools objectively.
> I can say, however, that C# has been moving forward every release, and those benchmarks demonstrate that it is separating itself from the Java/Go tier
I also agree. I have been following the development of C# for a very long time. I like what I have seen, especially since .NET Core. As I have mentioned in this thread already, C#’s performance is impressive. I just don’t accept a general claim that it’s as fast as Rust at this point, but not every application needs that much performance. I wish I could get some real world experience with C#, I just haven’t found any interesting tech jobs that use C#… and the job market right now doesn’t seem great, unfortunately.
I have hopes that adoption outside of stodgy enterprises will pick up, which would of course help the job situation (in due time of course).
Sometimes it's hard to shake the rep you have when you're now a ~25 year old language.
Awareness takes time. People need to be told, then they need to tinker here and there. Either they like what they see when they kick the tires or they don't.
I'm pretty language agnostic tbh, but I would like to see it become a bit more fashionable given its modernization efforts, cross-platform support, and MIT license.
Please read through the description and follow-up articles on the BenchmarksGame website. People did submit benchmarks, but submitting yet another SIMD+unsafe+full parallelization implementation is not the main goal of the project. However, this is precisely the subject (at which Go is inadequate) that we are discussing here. And for it, my suggestions in the previous comment stand.
> the GC very rarely interferes with my applications SLO
Somewhat random data point, but coraza-waf, a WAF component for e.g. Caddy, severely regresses on larger payloads and the GC scaling issues are a major contributor to this. In another data point, Twitch engineering back in the day had to do incredibly silly tricks like doing huge allocations at the application start to balloon the heap size and avoid severe allocation throttling. There is no free lunch!
Go's GC also does not scale with cores linearly at all, which both Java and .NET do quite happily, up to very high core counts (another platform that does it well - BEAM, thanks to per-process isolated GCs).
The way Java GCs require an upfront configuration is not necessarily the only option. .NET approach is quite similar to Go's - it tries to provide the best defaults out of box. It also tries to adapt to workload profile automatically as much as possible. The problem with Go here is that it offers no escape hatches whatsoever - you cannot tune heap sizes beyond just limits, memory watermark, collection aggressiveness and frequency, latency/throughput tradeoff and other knobs to fit your use case the best. It's either Go's way or the highway.
Philosophically, I think there's an issue where if you have a GC or another feature that is very misuse-resistant, this allows badly written code to survive until it truly bites you. This was certainly an issue that caused a lot of poorly written async code in .NET back in the day to not be fixed until the community went into "over-correction". So in both Java and C# spaces developers just expect the GC to deal with whatever they throw at it, which can be orders of magnitude more punishing than what Go's GC can work with.
The .NET GC is impressive in its ability to keep things running longer than they probably should.
In most cases with a slow memory leak I've been able to negotiate an interim solution where the process is bounced every day/week/month. Not ideal, but buys time and energy to rewrite using streams or spans or whatever.
The only thing that I don't like about the .NET GC is the threshold for the large object heap. Every time a byte array gets to about 10k long, a little voice in my head starts to yell. The #1 place this comes up for me is deserialization of large JSON documents. I've been preferring actual SQL columns over JSON blobs to avoid hitting LOH. I also keep my ordinary blobs in their own table so that populating a row instance will not incur a large allocation by default.
How much of the .NET GC's performance is attributable to hard coding the threshold at 85k? If we made this configurable in the csproj file, would we suffer a severe penalty?
> I've been preferring actual SQL columns over JSON blobs to avoid hitting LOH. I also keep my ordinary blobs in their own table so that populating a row instance will not incur a large allocation by default.
Are you using Newtonsoft.Json? I found System.Text.Json to be very well-behaved in terms of GC (assuming you are not allocating a >85K string). Also 10k element byte array is just ~10KB still. If you are taking data in larger chunks, you may want to use array pool. Regardless, even if you are hitting LOH, it should not pose much issues under Server GC. The only way to cause problems is if there's something which permanently roots objects in Gen2 or LOH in a way that, beyond leaking, causes high heap fragmentation, forcing non-concurrent Gen2/LOH collections under high memory pressure, which .NET really tries to avoid but sometimes has no choice but doing.
> How much of the .NET GC's performance is attributable to hard coding the threshold at 85k? If we made this configurable in the csproj file, would we suffer a severe penalty?
You could try it and see, it should not be a problem unless the number is unreasonable. It's important to consider whether large objects will indeed die in Gen0/1 and not just be copied around generations unnecessarily. Alternate solutions include segmented lists/arrays, pooling, or using more efficient data structures. LOH allocations themselves are never a source of the leak and if there is a bug in implementation, it must be fixed instead. It's quite easy to get a dump with 'dotnet-dump' and then feeding it into Visual Studio, dotMemory or plain 'dotnet-dump' analyze.
It's not that Go doesn't provide escape hatches out of malice, it just doesn't really _have_ them. Its GC is very simplistic and non-generational, so pretty much all you can control is the frequency of collections.
I think the difference is that what VSCode is doing is not an SSH Session like you get in a terminal with the ssh command or putty.
VSCode is installing a remote agent on the target machine that happens to use ssh as its transport protocol, and offers to share that transport with the user.
Is this a problem? Not if it only does things you want it to do. However any agent based system exposing an arbitrary API is suddenly a much bigger attack and risk surface area than the well trod (and still fraught) path of emulating a terminal over ssh.
> However any agent based system exposing an arbitrary API is suddenly a much bigger attack and risk surface area than the well trod (and still fraught) path of emulating a terminal over ssh.
I can see how this increases local (to the remote system) attack surface, but as long as the agent has the same OS privileges as the user logged in over SSH, what extra remote risk does this introduce?
Because if the Agent code is compromised, the fact that it leaves things behind is enough for an attacker to hide whatever they need along with the vs code blob. Vscode does this for the right reason, mostly it’s so the bulk of it runs on the host where you’re doing remote development or WSL or whatever. But like a lot of dev stuff these days, compromise the npm packages and bingo you can own all the machines.
Npm is already a terrible thing because the packages are managed so haphazardly, but now you’re exposed to the nonsense without even going anywhere near the mad rodeo of node. I like vscode but it’s not going anywhere near a machine I care about.
The argument is that you're running code on the remote host, and it could be compromised. The same argument can be made about any code you run on the remote.
VSCode may be seen as a larger attack vector due to its popularity; but maybe not as many won't use the SSH agent? It's also fairly common sense that you should never run it to mount on a production resource; but again, you shouldn't be able to ssh into a production machine anyway.
We usually don't hand over full ssh sessions to third party programs, so while you're right, I think people are not used to this level of trust into an app.
The article was to me a good reminder that it's a whole other level of access than just remote mounting a file system.
A user that can manage remote processes is generally going to have pretty high permissions.
For example, opening up debug ports on the running server processes, sudo privileges, or just the ability to run arbitrary code or curl in random binaries from the internet.
I think the latest Jetbrains tools do the same or similar things. They also install their own server on the target machine and connect to that. And I mean it's Jetbrains, so again, closed source tools. But it's not Microsoft so nobody is talking about it I guess.
I think you are talking about https://www.jetbrains.com/remote-development/gateway/ which requires a separate manual install on the client followed by a manual installation on the server during setup. It is not part of the regular IntelliJ IDE as far as I can see.
It isn’t. I mean yeah gateway is what you described. But the functionality is also included in Jetbrains IDEs and doesn’t require any manual install on the server. It installs its own thing exactly like VSCode does.
I can't find any such thing in PhpStorm v2024.3, can you tell me the feature name? Is it this you are thinking of (which requires some clear manual steps)?
If you're thinking about JetBrains' remote development, it's actually the IDE (a headless version) that's installed on the remote dev machine. Since you first need to install the IDE on the dev server first, you are never under any illusion that it's a simple SSH-based remote editing à la vim (even though it also relies on SSH).
Is it better to programmatically interact with bash to provide the features VSCode does? Do note that I am unwilling to accept an implementation with less features/ease of use!
I can see how writing a custom agent that provides remote access to privileged API's is a bad idea but bash isn't exactly the most secure piece of software in the world.
Bash is not running though. You might get a Bash session once you connect via SSH, but it just sits there waiting for you to input commands, while the VS Code installed agent thing does network stuff on its own iiuc. Bash is not acting as a server, afaik.
This is really big. I always thought Vscode ssh capability was just some black magic that the open source community wasn't able to replicat well with vscodium extensions. Yet, it seems the reason why is that MS threw a curve ball with that naming.
It didn't kill Microsoft. Microsoft isn't dead. However Microsoft does now have competitors. The takeaway here is that antitrust is fantastic for consumers and innovation.
> Why spend 2-10x as much to live in one of four coastal metros
I think you answered your own question: it’s not (entirely) an economic decision. Weather, culture, nature, civic amenities. There’s a lot in life that money can’t buy. Sure I could own a McMansion on a palatial plot of prairie, but what if the square footage of my house and acreage of my yard isn’t what’s important to me?
Being surrounded by Christian conservative Trump voters is not how everyone wants to live their life.
HN probably consists of a lot of people who are perfectly content spending their entire life looking at a screen perfectly oblivious of what is going on.
There are many religious questions people probably are scared to ask a human. Imagine any closeted gay or trans kid wanting to know if the way they feel is “wrong.” Just asking the question to your parents or a faith leader could raise suspicions and be dangerous.
So while I wish the world were one in which people didn’t have a reason to be scared to ask questions, that’s not the world we live in. Are LLMs an answer? I have no idea, but I understand the impulse to try.
> Imagine any closeted gay or trans kid wanting to know if the way they feel is “wrong.” Just asking the question to your parents or a faith leader could raise suspicions and be dangerous.
Depends on the religion and the place. A Catholic could ask in the confessional with the seal of secrecy. Anyone could ask online anonymously. In most churches I know it would not be dangerous to ask questions, and you can do in confidence one way or another.
I really do not think LLMs are anything like the answer for people grappling with personal issues. You need someone supportive with empathy.
Also, who trusts an LLM to give them the right answer? Can an LLM guide someone gently to think things through? Can it refuse to give someone a black and white answer when they want a simple answer to a complex question?
I think an LLM is the dangerous option.
Different for people in very conservative cultures, of course.
Yeah... I hate to be a downer in this but neither humans nor faceless tech are good approaches to keep someone safe, especially when they are hated just for who they are by a major faction.
Or worse, who would track people with questions like this and try to "teach" or "purify" them. Even without violence this is vile, and yet I fully believe this happens just like some US states spending tax money trying to estimate constituent periods to see if they may have had an abortion.
It's an interesting approach but also a big contrast to the typical community style formula for religion / even just different and disconnected sort of evangelism where you have no idea what the result is...
Nuclear requires an extremely mature bureaucracy to handle the serious longterm risks efficiently. Here in the USA I'm just not sure our government is mature enough for the task. Every 4 years we may have someone come in to slash safety regulations or erect impossible regulatory barriers, not to mention all of our defense treaty obligations and their churn. Every year (or less!) our congress may decide to let the government shutdown and stop paying critical contractors. Trains carrying spent or unspent fuel may derail thanks to inefficient regulation. etc etc.
One of the massive benefits of wind and solar is that it can be scaled down to a size even a consumer can deploy. This makes it relatively resilient to regulatory whims compared to massive undertakings like nuclear. I'm not a nuclear engineer, but I suspect the recent emphasis on smaller nuclear plants has more to do with navigating regulatory hurdles and uncertainty than efficiency.
All that to say I am quite jealous of countries with mature bureaucratic regimes that can efficiently manage nuclear!
I’ve never understood the use of SSE over ndjson. Builtin browser support for SSE might be nice, but it seems fairly easy to handle ndjson? For non-browser consumers ndjson is almost assuredly easier to handle. ndjson works over any transport from HTTP/0.9 to HTTP/3 to raw TCP or unix sockets or any reliable transport protocol.
Manually streaming a XHR and parsing the messages is significantly more work, and you lose the built-in browser API. But if you use a fetch ReadableStream with TLV messages I'm sold.
The article goes into great detail about the benefits of an opaque api vs open structs. Somewhat unintuitively open structs are not necessarily the “fastest” largely due to pointers requiring heap allocations.
Opaque APIs can also be “faster” due to lazy loading and avoiding memcpy altogether. The latter appears in libraries like flat buffers but not here IIRC.
A quick search of “c++” on HN shows many C++ related posts explicitly mention the language. I would assume many other language specific posts also name the language they’re using to save uninterested parties a click. There’s really nothing particularly interesting about this: it’s a fairly natural pattern in a multilingual community like HN.
As someone who is not directly paying the bills but has wasted far too much of my life staring at JVM GC graphs and carefully tuning knobs, I vastly prefer Go’s opinionated approach. Obviously not a universally optimal choice but I’m so thankful it works for me! I don’t miss pouring over GC docs and blog posts for days trying to save my services P99 from long pauses.
reply