Would be interesting if C# now would be a viable alternative to C++ for them.
Would be interesting if C# now would be a viable alternative to C++ for them.
1) we use intrusive containers, so memory managing container data structures is collocated with actual data. 2) memory allocation is not tied to GC, so we don't get pauses 3) there's almost none synchronization between different threads and there are (almost) no globals. For a story why globals are a killer for performance, read https://www.p99conf.io/2021/09/28/hunting-a-numa-performance... 4) the previous is only possible with existence of a user space scheduler which guarantees that specific threads are pinned to a single CPU. Also, there's no need to call mmap multiple times, as Seastar (concurrency framework written with Scylla in mind) allocates whole system memory and takes advantage of overcommitting in Linux. There's no syscall at memory allocation, just some userspace work and a possible page fault.
I'm not sure whether C# can do away with these problems? Let me know if you know. That being said, modern C++ is really convenient. Not anything you've seen 15 years ago in university.
As for C# you can do most C++ like stuff in C# 10.
Regarding C#, if you really want to learn how to do C++ style programming in C#, have a look at the documentation regarding C# 7.0 - 7.3, C# 8, C# 9 and C# 10 regarding readonly structs, span, stackalloc in safe code, blittable types, GC free regions, malloc/free calls, allocation free memory pipelines, in and return ref types, local references, using pattern (implementing IDispose is no longer required)
Regarding classical C# (what is available until .NET Framework 4.8), you have structs, value types, manual memory management via System.Runtime.InteropServices.
You can start at the free posters here, https://prodotnetmemory.com/
You mean like C++11? So C++ standard from 11 years ago ? or C++14? C++17? Last time I checked UW was like 17 years ago so maybe things changed but back then they were like 10+ years behind industry in practical terms.
For instance see this: https://pkolaczk.github.io/overhead-of-optional/
As for databases, C++ has not only a performance edge over Java (and possibly .NET) but it also offers superior non-memory resource management capabilities. Databases manage a lot of resources that are not memory, and RAII is a game changer.
Especially when you return a value, it is more than likely to escape.
* branch elimination with cmov
* loop unrolling
* SIMD vectorization
* turning heap allocation into stack allocation
All those things could be done without breaking any semantic guarantees of Optional even without value types in place.
Also note how even forcing the Rust program to use references with double Box didn't make the code any worse. So Rust/LLVM had no issue optimizing that out even if Option was defined the way it is in Java now.
Of course you have less time/resources during JIT compilation (and mostly, inline depth), so the quality of the resulting code can at times be vastly worse than what an AOT compiler can do, but my experience is that in real life code bases Java’s JIT compiler is really great, while this benchmark reflects on a singular case where it failed.
In theory - yes.
In my experience it just repeatedly does worse job than a C / C++ / Rust compiler, unless I'm very careful in Java coding (yes, I can often make it close, but this requires way non-idiomatic Java code; e.g. I've seen cases when manually unrolling a loop helped getting 2x more performance, which is something I don't recall ever having to do in C / C++ / Rust).
For example we don't use Java Streams in performance critical code, because everybody on the team knows it does not optimize them back to the level of simple for loops. Well, we checked many times and it simply never happened, although, theoretically it could. But I can throw a chain of map/filter/fold calls in C++ or Rust freely and it just works as fast as a hand-optimized loop, with unrolling, simd, etc.
(But I’m fairly sure you know that already)
For context, see Scala's battle with specialization to get reasonable performance of collection transformations. Once you start using lambdas to define e.g. a filter condition, and once you want generic implementations working on different item types, this pushes you into a boxing hell and the JVM is surprisingly reluctant to remove all that overhead, and you end up with >10x penalty. So instead of relying on JVM, they specialize data structures for primitive types. It is even something that you are supposed to do in Java manually (see IntStream, LongStream classes).
Longer-lived objects still need (trickier) manual deallocation.
No it isn't. With RAII, you can look where an object gets constructed and know exactly where it will be destructed. With garbage collection, you can't, and in fact there's no guarantee that it ever will be. Also, with garbage collection, you can save references to whatever you like, wherever you like, for as long as you like. With RAII, you need to make sure you don't create any dangling references or use any dangling pointers.
As an extreme example (that I have occasionally thought about doing though probably won't), you could fork TypeScript and add ownership and lifetime and inherited-mutability annotations to it, and have the compiler enforce single-ownership and shared-xor-mutable except in code that has specifically opted out of this. As with existing features of TypeScript's type system, this wouldn't affect the emitted code at all—heap allocations would still be freed nondeterministically by the tracing GC at runtime, not necessarily at the particular point in the program where they stop being used—but you'd get the maintainability benefits of not allowing unrestricted aliasing.
(Since you wouldn't have destructors, you might need to use linear instead of affine types, to ensure that programmers can't forget to call a resource object's cleanup method when they're done with it. Alternatively, you could require https://github.com/tc39/proposal-explicit-resource-managemen... to be used, once that gets added to JavaScript.)
Of course, if you design a runtime specifically to be targeted by such a language, more becomes possible. See https://without.boats/blog/revisiting-a-smaller-rust/ for one sketch of what this might look like.
Not to mention, Copying GC also solves memory fragmentation, which C++ still suffers from unless you also design your allocations carefully around sizes of types.
With or without RAII you should design your program around who owns each object, unless you want to end up with unmaintainable mess leaking file descriptors, network sockets, native memory buffers or trying to access resources after closing them. Which is why Cassandra and Netty implement their own reference counting.
> Not to mention, Copying GC also solves memory fragmentation
Not really. It only moves the problem elsewhere so it doesn't look like fragmentation. Compacting GC needs additional memory to have a room to allocate from, and that amount of memory is substantial unless you want to do more GC than any useful work. Also it is not free from fragmentation most of the time - the heap is defragmented only at the moment right after compaction. As soon as your program logically frees a memory region (by dropping a path to it), you have temporary fragmentation until the next GC cycle, because that region is not available for allocation immediately. And there is internal fragmentation caused by object headers needed to store marking flags for GC - which can consume a huge amount of memory if your data is divided into tiny chunks.
> which C++ still suffers from unless you also design your allocations carefully around sizes of types
Modern allocators split allocations into size buckets automatically.
But I agree: for anything that you want to deallocate deterministically, or at least soon enough, you need to track ownership, and care about the lifetimes. Such objects are relatively few, though.
Depends on the use case. Not if you're storing them in a long living collection. Also heap allocation is costly, even in languages with fast heap allocation. It is still an order of magnitude slower than stack allocation.
> But I agree: for anything that you want to deallocate deterministically, or at least soon enough, you need to track ownership, and care about the lifetimes
It is not only that. You need ownership not only to determine lifetimes.
You need to know it in order to be able to tell if, having a reference to an object, you're allowed to update it and in what way. Is it the only reference? If it is shared, who also has it and what can it do with it? If I call "foo" on it, will I cause a "problem at a distance" for another shareholder? Being able to answer such questions directly by looking at the code makes it way easier to navigate in a big project written by other people.
In C++ if I can see a simple value or a value wrapped in a unique_ptr, I know that I can update it safely and nothing else holds a reference. If I see a shared_ptr, I can expect it is shared, so I have been warned. The intent is clear. In Rust it is even safer, because the compiler enforces that what I see is really what I get (it is not just relying on conventions).
On the flip side, GC-based languages tend to invite a style of coding where reference aliasing is everywhere and there are no clear ownerships. I can see a reference to something and I have no idea what kind of reference it is and what I can safely do with it. It is just like a C pointer. I need to rely on code comments which could be wrong (or read a million lines of code).
Don’t get me wrong, I really like RAII or Rust’s compiler enforced ownership model but it doesn’t solve everything. Eg. it only disallows data races not race conditions.
Also, immutability goes a long way toward solving all that.
Not in the case of a mark-compact collector, which works entirely in place, or a mark-region collector such as Immix [0], which only copies a small fraction of the heap.
> Also it is not free from fragmentation most of the time - the heap is defragmented only at the moment right after compaction.
An improvement would be to to perform more frequent "partial" collections, such as in the Train algorithm [1]. But some collectors (such as Immix again) avoid compaction until fragmentation is considered bad enough, which seems like a fair compromise.
> And there is internal fragmentation caused by object headers needed to store marking flags for GC - which can consume a huge amount of memory if your data is divided into tiny chunks.
The description of Doug Lea's allocator [2] suggests there are also "object headers" of a sort on allocated data in dlmalloc. You could probably steal mark bits from those headers, but it is commmon to use a separate marking bit/bytemap which is separate to space where objects are allocated, and thus has none of the fragmentation you describe.
[0] https://www.cs.utexas.edu/users/speedway/DaCapo/papers/immix...
The mutator always allocates from a contiguous memory region. It can't allocate from the memory that was logically released, but not yet collected. So it needs more total memory than the amount of live memory in use at any time, unless you have an infinitely fast GC (which you don't have). In order to avoid too frequent GC cycles, or to allow it to run in the background, you need to make that additional amount of memory substantial.
JVM GCs typically try to keep low GC overhead (within single %), which often results in crazy high memory use, like 10x the size of the live memory set.
> but it is commmon to use a separate marking bit/bytemap
Sure, you can place it wherever you wish, but it still requires additional space.
But malloc and friends usually do what a very basic GC would (make separate pools for differently sized “objects”).
While object pools also need much more memory unless it is full.
So all in all, GCs do trade off more memory for more efficient allocation/deallocation but that is a conscious (and sane) tradeoff to make for like 99% of applications as memory stored on RAM doesn’t consume much energy compared to doing GC cycles like a mad man. Also, it is quite configurable in case of JVM GCs.
As for the energy efficiency, I seriously doubt that bringing all memory into cache once in a while, including memory that is not needed frequently by the application, only in order to find live vs dead memory is all that energy efficient. The allocation itself is indeed typically slightly faster but the marking and compaction is additional cost you don't have to pay in manual memory management.
Also using generational algorithms does not remove the dependency of the memory overhead on the allocation rate. Those techniques improve the constant factor, but it is still an O(N) relationship, vs O(1) for a manual allocator. If the allocation rate is too high there are basically two solutions: (1) waste more memory (use very big nurseries, oversize the heap) or (2) slow down / pause the mutator.
The industry seems to prefer (1) so that probably explains why I never see Java apps using <100 MB of RAM, which is pretty standard for many C, C++ or Rust apps; and 50x-100x memory use differences between apps doing a similar thing are not that uncommon.
I may very well be wrong, but I don’t think it is any worse than the occasional OS scheduling/syscall, etc. GCs happen very rarely (unless of course someone trashes the GC by allocating in hot loops)
Also, while a destructor is indeed O(n) it is a cost that has to be paid on the given thread, while GCs can amortize it to a separate thread.
I thought we were discussing fragmentation, i.e. where we put marking bits.
Not feeding the troll but outside memory safety, everything else you list exist in the C++ ecosystem with generally better alternative than in C#.
And as soon as you do touch mmap , unsafe area or native code in C# you loose memory safety too anyway.
I didn't spent a lot of hours in C++ world, but it never felt simple
- N compilers, N package managers, N ways to do everything
C++ is not simple.
But presenting C# (or Java) as "simple" is equally hypocritical. The JVM or the CLR and their associated frameworks are monster of complexity, engineering and legacy that require close to an entire lifetime to be mastered entirely.
C# (or Java) are "accessible", meaning a newbie devlopper can produce something halfway baked in these languages relatively quickly.
And this is something you can not say about C++.
But they are not in any way "simple".
And you don’t have to be a master of the JVM because chances are you are not a gcc/clang maintainer and yet you can write performant-enough correct code.
Just because JVM or CLR are complex, then it *doesn't* mean that writting good C# / Java requires you to be proficient at CLR/JVM lvl and because of that it is hard.
>meaning a newbie devlopper can produce something halfway baked in these languages relatively quickly.
Newbie developer can produce mediocre solutions in all of those - C#, Java, C++.
The difference is that in C#/Java world it may be slow and in C++/C world it may be exploitable (more likely) <snark>.
Anyway, in my world very often it's not about internals, but about modeling skills, about OOP, testability. Those are some of the ways of measuring how good the code is.
Good system modeling skills are way above technology
What do you mean? since .NET Core it always worked flawlessly for me
>unacceptable code formatter
hmm? that's preference not an argument.
No, that's not a preference. I'm not complaining about a lack of options, I really don't care how code looks, if it all looks the same. And it fails at that. It quite often simply takes the code as it is and indents it a little bit. Clang-format (and rustfmt and dart format and plenty of others) give you the nice, tidy and homogeneous code layout i expect from a auto formatter.
iirc there were some changes around .NET Framework -> .NET Core to how does it work (where packages are stored) and that's why I said that since .NET Core I didn't have problems with it
Oh? I don't think you can dispute that the C standard library is very limited and the state of dependency management / build tooling is very poor. And that actually limits the usability of the open source library ecosystem quite a lot; maybe there are more C++ libraries out there, but you can't just type what you want into the nuget search bar and get on with using it.
Simplicity is in the eye of the beholder, but the very weak semantics of C++ templates mean you can't reason compositionally about C++ code, whereas in C# it's relatively easy to have a codebase that you can reliably understand piecemeal.
> And as soon as you do touch mmap , unsafe area or native code in C# you loose memory safety too anyway.
In principle yes, but if you keep those points very rare then you can subject them to extra review etc. at a level that would be impractical with a C++ codebase (where even "a + b" is undefined behaviour in the general case). Memory safety vulnerabilities in real-world C# codebases are rare.
That seems a little questionable. Maybe "sometimes better"?.