Hacker News new | past | comments | ask | show | jobs | submit | mtklein's comments login

I'd suggest working incrementally from areas of your existing strength. Tweak whatever code base you are most familiar with, starting with a tiny change, and see how the assembly changes. I use objdump -d and git diff --no-index for this all the time.


I think this is actually one of the selling points of C over other languages. Certainly it's easy to fool yourself into thinking you know any language better than you do, but C genuinely is relatively tiny, and I think it's reasonable to expect to become familiar with almost everything in the language in a month or so of full-time work. After a decade of day job C++ coding, I'd not say the same for myself there.

Rust and C++ lately have been particularly quickly moving targets to keep up with, where C is still pretty much the same as C99. The compilers are better, their warnings are better, sanitizer modes are incredible, but the things you type into your text editor to express your program structure are still pretty much the same thing.


I could have been more precise when I said it's easy to fool yourself into thinking you know _any_ language better than you do... both rustc and ghc have made it abundantly and repeatedly clear that I don't know Rust and Haskell well, and the "if it compiles, it's probably correct" trust that develops from that frustration is a great asset to both those languages.

But those are exceptions: I've definitely felt like I've known more than I really did about at least C, C++, Java, Scheme, Python, Go, bash, Javascript, cron, and several assembly variants, and those platforms did little to stop me from learning about my inexperience in difficult, sometimes embarrassing, sometimes expensive ways.

I'm a big fan of more compile time analysis any way it can be done, and whatever can't at runtime. More types, more proofs, more fuzzing, more sanitizers, and also where possible, more restricted execution models than Turing-complete. Any code that can't prove itself correct is a serious liability. I just also like C... it's got many footguns, yes indeed, it's practically constructed out of only footguns, but there are only like 8 of them, and they're all right there waiting for you to get to know them. It's like having a few good sharp knives in your kitchen. You get to know each well, using them one at a time---and if any is missing from the block you pause everything and look around very carefully.


> but C genuinely is relatively tiny,

Relatively tiny and with a disproportionate number of footguns.


Not really that much In my experience. Most of the unsafety is very obvious and most of the memory safety really come from errors from this obviously unsafe parts, e.g. the most common memory safety bug is pointer arithmetic gone wrong. The others are temporal memory safety and signed integer overflows. None of this is really subtle or what I would really call a footgun. Nowadays, programmers do not seem to understand integer conversion rules of C anymore (although they are simply IMHO), but then this is something which can be mitigated via sanitizers.


I doubt that a month of so of full work will get someone fully proficient on e.g. the finer details of integer promotions and how they can break things in non-obvious ways (think of situations such as combining signed and unsigned operands of different widths). The fundamental problem is that sane production code written in C will generally deliberately avoid some aspects of the language precisely because they are so footgun-prone, which means that if you learn solely by experience, you may never even become aware of the footgun behind the pattern.


Looking at the GPU tech used and the committer names in that repo, I'm pretty confident this will at very least compete with both Skia and Pathfinder. I'm not super familiar with Vello.

These are solid tech choices by people who know what they're doing... definitely worth the effort to check its performance and quality out for yourself.

These GPU-first renderers all have slightly different approaches and each probably occupies a best-in-class niche for some multidimensional space of quality x performance x hardware/driver support x ease of integration. If you're not happy with one of the four, the others are all worth checking out.


> I'm pretty confident this will at very least compete with both Skia and Pathfinder

Couldn't Vello be embed / used by Skia, Cairo et al, if those renderers wanted to use GPU Compute instead of CPU's for preprocessing?


Skia does include some support for using Vello for rendering but it's just experimental, not enabled by default.


One bit I find easy to lose sight of is that if I do spend my day cultivating wheat for flour for a cake, to make sure to eventually make and eat that cake, and not just spend all my time getting better at cultivating more and more wheat.


I dunno, I've discovered a deep joy in cultivating, to the point where it has developed into my "cake" to simply cultivate. I don't mind if someone else ultimately reaps the benefits, since from my own vantage I won't regret having spent an extra day in the field, doing the work.

To me, this is the goal; to be able to look up towards the sun every day, sore muscles and sweat dripping, and say, "thank you for this day."

Metaphorically, I mean. In truth I'm a cave-dwelling grug-minded developer.


Thank you for leading me to https://grugbrain.dev/


Chapeau!!!! :)


Yes! It's like the problem of when to stand up from the blackjack table.


Might try Simulacron-3.


On the topic of finding Bayes' theorem intuitively, I can never remember it on its own, but starting from the joint probability P(A,B) for any arbitrary A and B always helps me:

    // A and B are arbitrary.
    P(A,B) = P(B,A)

    // These two make sense if I sound them out in words.
    P(A,B) = P(A) P(B|A)
    P(B,A) = P(B) P(A|B)

    // Combine to give Bayes' theorem.
    P(A) P(B|A) = P(B) P(A|B)


The advantage of the odds formulation is that you can compute posterior odds in your head, whereas computing the posterior probabilities via the standard form of the theorem (that you wrote down here) involves an unpleasant normalization factor.

E.g. for the example in https://en.wikipedia.org/wiki/Base_rate_fallacy#Low-incidenc...:

    prior odds of infection =   2 :  98
  × likelihood ratio        = 100 :   5
  = posterior odds          ≈ 200 : 500
and thus the probabilities (if you actually need them at this point) are ≈ (2/7, 5/7).

See also 3Blue1Brown’s exposition: https://youtu.be/watch?v=lG4VkPoG3ko.


Exactly! The mathematical aspect of Bayes' Theorem is a simple algebraic manipulation of conditional probabilities.

Another way to intuitively justify your middle two equations is to visualise a Venn diagram with overlapping circles representing A and B, such that areas correspond to probabilities. Then P(B|A) is the area of the overlap - i.e. P(A,B) - divided by the area of A - i.e. P(A).


I agree that (void)bla is desirably clear, pithy and unambiguous. And I like something like this if I'm dealing with functions with more than a couple unused parameters:

    static void unused_(int x, ...) { (void)x; }
    #define unused(...) unused_(0, __VA_ARGS__)
This way you can just have one `unused(foo,bar,baz,quux);` line at the top of the function listing all unused parameters.


If you've got control over the type of the pointee and not just the pointer, __attribute__((packed)) can work for this.

    struct foo {                         int x; };
    struct bar { __attribute__((packed)) int x; };

    _Static_assert(_Alignof(struct foo) == 4, "");
    _Static_assert(_Alignof(struct bar) == 1, "");


Yeah, I end up using __attribute__((packed)) for this at work. For tortured reasons, part of our codebase allocates memory with only 8-byte alignment, but the buffer is cast to a type that would have 16-byte alignment without __attribute__((packed)). As a result Clang wants to generate VMOVDQAs that require 16-byte alignment, unless you use packed, in which case it generates VMOVDQU.


Ordinarily you're at the whim of the optimizer whether calls in tail position are made in a way that grows the stack or keeps it constant. musttail guarantees that those calls can and are made in a way that does not let the stack grow unbounded, even without other conventional -On optimization. This makes the threaded interpreter design pattern safe from stack overflow, where it used to be you'd have to make sure you were optimizing and looking carefully at the output assembly.

If nothing else musttail aids testing and debugging. Unoptimized code uses a lot more stack, both because it hasn't spent the time to assign values to registers, but people often debug unoptimized code because having values live on the stack makes debugging easier. The combination of unoptimized code and calls in tail position not made in a way that keeps stack size constant means you hit stack overflow super easily. musttail means that problem is at least localized to the maximum stack use of each function, which is typically not a problem for small-step interpreters. Alternatives to musttail generally involve detecting somehow whether or not enough optimization was enabled and switching to a safer, slower interpreter if not positive... but that just means you're debug and optimized builds work totally differently, not at all ideal!


My only major objection to C++ magic statics is that you can only use them for statics. Sometimes you find yourself with a task that needs to be done once per object instead of globally, and you've got to roll your own, and then you might as well use that same mechanism for globals too.

My minor objection to magic statics is that you don't have control over the size or packing of the control word, and it usually ends up being something awkwardly mid-sized like a size_t where normally you'd want it either as small as possible or cache-line sized.

I like to handle this something like this:

     void once(char _Atomic *flag, void (*fn)(void *ctx), void *ctx) {
         char st = atomic_load_explicit(flag, memory_order_acquire);
         if (st == 0 && atomic_compare_exchange_strong_explicit(flag, &st, 1,
                                                                memory_order_acquire,
                                                                memory_order_acquire)) {
             fn(ctx);
             atomic_store_explicit(flag, st=2, memory_order_release);
         }
         while (st != 2) { st = atomic_load_explicit(flag, memory_order_acquire); }
      }
There's a commented version here: https://github.com/mtklein/best/blob/main/once.c


You can use std::once_flag and std::call_once from <mutex> instead of rolling your own atomic solution.


What does C++ have to do with this submission?


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: