664 karma · joined August 27, 2010
My knowledge is dated and second hand though. New GPU APIs hopefully changed this!
The other day, alone in a city I'd never been to before, I snapped a photo of a bistro's daily specials hand-written on a blackboard in Chinese, copied the text right out of the photo, translated it into English, learned how to pronounce the menu item I wanted, and ordered some dinner.
Two years ago this story would have been: notice the special board, realize I don't quite understand all the characters well enough to choose or order, and turn wistfully to the menu to hopefully find something familiar instead. Or skip the bistro and grab a pre-packaged sandwich at a convenience store.
bool equiv(float x, float y) {
return (x <= y && y <= x)
|| (x != x && y != y);
}
This handles things like ±0 and NaNs (while NaNs can't be IEEE-754-equal per se, they're almost always interchangeable), and convinces -Wfloat-equal you kinda know what you're doing. Also everything visually lines up real neat and tidy, which I find makes it easy to remember.Outside unit tests... I haven't really encountered many places where float equality is actually what I want to test. It's usually some < or <= condition instead.
$ /opt/homebrew/opt/llvm/bin/clang-20 -O1 -c union.c -o union.o && objdump -d union.o
union.o: file format mach-o arm64
Disassembly of section __TEXT,__text:
0000000000000000 <ltmp0>:
0: f900001f str xzr, [x0]
4: d65f03c0 ret
0000000000000008 <_create_d>:
8: f900001f str xzr, [x0]
c: d65f03c0 retI am basing this entirely on memory and the wikipedia article on type punning. I welcome extremely pedantic feedback.
$ cat union.c && clang -O1 -c union.c -o union.o && objdump -d union.o
union foo {
float f;
double d;
};
void create_f(union foo *u) {
*u = (union foo){0};
}
void create_d(union foo *u) {
*u = (union foo){.d=0};
}
union.o: file format mach-o arm64
Disassembly of section __TEXT,__text:
0000000000000000 <ltmp0>:
0: b900001f str wzr, [x0]
4: d65f03c0 ret
0000000000000008 <_create_d>:
8: f900001f str xzr, [x0]
c: d65f03c0 ret union {
int bits;
float f;
} pun = {x}, scale = {0x47000000};
pun.bits |= scale.bits;
pun.f -= scale.f;
pun.f *= (256/255.0f);
This basically amounts to a software implementation of int->float conversion instructions; sadly I have never found a spot where it's actually worth doing when you have those int->float instructions available already, even with the FMA as a single instruction.It's also worth considering whether your application can handle approximate conversion. If you have a [0,255] unorm in x, x + (x>>7) or equivalently x + (x>0x7f) will round it to a [0,256] fixed-point value. Crucially, this rounding does handle 0x00 and 0xff inputs correctly. Once in fixed-point with a nice power-of-two divisor, you can play all sorts of tricks, either again making use of the bit representation of floats, using ARM fixed-point instructions, etc. If you've ever looked longingly at the pmulhrsw family of instructions, this is a ripe area to explore.
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.
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.
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.
// 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) 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. struct foo { int x; };
struct bar { __attribute__((packed)) int x; };
_Static_assert(_Alignof(struct foo) == 4, "");
_Static_assert(_Alignof(struct bar) == 1, "");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 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.cFor me the real key is the last part. If you're happy building and testing with sanitizers from source, I don't think there's much more to gain by adding Valgrind to the mix, but if sanitizers aren't available or you find yourself with a binary and not source, I'd check out Valgrind.
Almost everyone at a place like MIT feels some impostor syndrome, and I think maybe even for good bogus-mathematical-theorem reasons: if a place like MIT tries to select from the top few percent of a normal distribution, that new distribution after selection will look bottom-heavy (and feel that way around campus). We all knew those few incredible superstars who could run laps around us, but that doesn't mean you're not a star too.