3 ms·
C is not too low-level at all. More low-level than Java, but no more than C++/Rust and other low-level languages. It’s hardly an assembler with macros in this c
by kaba0 4y ago
C is not too low-level at all. More low-level than Java, but no more than C++/Rust and other low-level languages. It’s hardly an assembler with macros in this century — it’s just the compiler at O3 doing so many mental acrobatics to make you believe that lie.
- haspok 4y agoI respectfully disagree. The mental model of writing assembly code and C code are very close - you can write the best C code if you think about how you would translate it to CPU instructions in your head. As a matter of fact, you said it yourself in a neighbouring thread that all C needs is a large array - I cannot imagine how can you get any lower than that: mapping almost 1:1 to the general memory model of a CPU executing instructions with some addressable memory. C++ has classes and templates, Rust has... a lot of things, they are definitely higher up in terms of abstractions built into the language.
- kaba0 4y agoRe C++/Rust: That’s expressivity, which is a different axis. C can’t control cache behavior, has no SIMD control, and no threading support (non-standard extensions doesn’t count) It maps well to PDP-11, but that has barely any similarity to modern CPUs. For what its worth, one might argue that C++ and Rust are lower level, as they can even control some of the above listed functions. As for “closeness to generated assembly”, as I said, at O3 the generated code doesn’t resemble the source at all — it might not even execute anything your wrote, it may have calculated the result at compile time and will just return the result/look it up in a map at runtime.
- dzaima 4y agoCache is a microarchitectural thing. There's no "controlling" it without explicitly knowing the properties of the caches of the running processor (size, latency, associativity, replacement policy, and whatever other quirks the hardware designers decide to add) and building your code around it, at which point nothing will be able to do it in a "standard" way (unless your standard hard-codes all the past, present, and future architectures, which, good luck). SIMD isn't really feasible to fully do in a standard way either. Without specific knowledge of the target, you can easily write SIMD-looking code that ends up slower than a scalar equivalent (e.g. if you do any non-constant shuffle on x86-64 pre-SSSE3 (i.e. the default), you'll get utterly awful results. On ARM NEON, a non-constant i32x4 shuffle is gonna be quite awful too). Threading - <threads.h> is a standard header, no? (granted, it doesn't appear to be used much, but it does exist) And, sure, every statement/expression/operator of yours doesn't end up always literally mapping to a single assembly instruction, but there's pretty much always a way it could, and quite often it does (and in the cases where it doesn't, it (usually) maps to something better than what you wrote). The main thing that doesn't is macros, but those are extremely trivially expandable.
- kaba0 4y agoYou can prefetch memory though (but yeah, it indeed is very finicky to get right. So is any sort of decision compilers do, so there is that). Off topic, but the OCaml GC does use prefetch instructions for I believe a 3-4x speedup in an already mature project, so there is that. SIMD is still the panacea of compiler optimizations, you won’t get significant performance improvements from trivial peephole optimizations like replacing * 2 with << 1, and the supposedly low-level C has no way of controlling that. Sure, vector sizes are different between processors, but so is many other things we already generalize/assume. But an optimizing compiler will change up your loops all around, inline your functions, etc, so just because it could correspond to some naive assembly, it won’t unless you compile it with some toy.
- dzaima 4y agoOk, prefetching is a thing that could maybe be standardized as a hint, but even then there could be differences in how many bytes apart your prefetches optimally need to be, and what range they'll prefetch (might even be a property of the specific CPU at runtime, at which point things get really hairy to do properly). My point with SIMD is that pretty much all current things do, at worst, map to one instruction (maybe more for things like large loads/stores) on modern 64-bit architectures. But with SIMD the operations supported by various architectures are so diverse, such that taking the intersection (i.e. preserving things mapping mostly-1-to-1 at worst), you'll have an unusable thing (no non-constant shuffles, 8-bit shifts, float rounding, movemask, fault-only-first loads, compress, limited conversions (for 64-bit floats x86 only has i32↔f64, but NEON only has i64↔f64; and narrowing integers takes a sequence of shuffles on pre-AVX-512 x86)). And if you take the union of architectures, a large amount of operations will just suck on any architecture that's not where it was from. Never mind architectures without SIMD. Different vector sizes is a comparatively trivial problem to tackle (but then you get to the scalable SIMD sets, i.e. SVE & RISC-V V; and even those differ in an important way - SVE vectors are some multiple of 128 bits long, up to 2048, but rvv also guarantees it being a power of two, and can be up to 2^16 bits, at which point indexes in the vectors don't fit in 8-bit integers). Sure, the optimizing compiler will optimize things, but that's what it does - optimize things, so its actions should be preferable, and manually writing something equivalent to what the compiler did is still very possible, and often not even that weird (with the discussed exception of vectorization).
- JohnFen 4y ago> but no more than C++/Rust and other low-level languages. Hmm, I disagree. C is a mid-level language, one level of abstraction up from assembly. C++ and Rust are high-level languages, one or two levels of abstraction higher than C.
- kaba0 4y agoThat depends on the definition one uses. The traditional, but quite useless one is everything above assemblies is high level, only those are low-level PLs. I prefer the one of “what features does the language give you idiomatic control of”. In that vein, Rust is more low-level than by having simd support.
- JohnFen 4y agoInteresting. I wasn't aware there was such a divergence in understanding of what counts as "low" or "high" level. I thought everyone just went by the amount of abstraction of the machine code. So machine code is no abstraction, assembly is one level up, C is another level up, etc. The less the language "looks" like machine, the higher the "level" of the language. The inclusion of SIMD support, in my view, falls in the mid-level category. It's abstracting the differing parallel computing implementations of different processors to make them all usable with the same code. That's not low-level in my book. I'm not saying this to say that your stance is incorrect -- the "level" of a programming language has never been a technically well-defined thing anyway. I'm just saying this to express that my eyes have been opened to a perspective I was unaware of.