Live data from Hacker News

How to Think About Variables in C

denniskubes.com

51–60 of 66 posts

Re: How to Think About Variables in C

#51

Earlier quoted context omitted.

> It's a valid operation regardless of whether a standards body says it's not. Whoa there, cowboy. You may not feel personally beholden to standards bodies, but compiler vendors are following their lead. The major compilers are getting more and more aggressive about optimizing away undefined behavior every year. > The effect is to set y to the first two bytes of memory from x. No, it's really not. It's undefined beha…

No, it's really not. It's undefined behavior and the compiler is free to do absolutely whatever it wants. The point is that compilers do some specific thing, regardless of the fact that the standards bodies say they're free to reboot your computer. As long as all you care about is x86/x86_64/PowerPC (and probably ARM as well), then you can trust that the compiler is going to generate code which copies the first two b…

There be dragons. The following program prints 10 on gcc 4.6.3, x86-64:

  #include 
  #include 

  void f(uint32_t *x, uint16_t *y) {
    *x = 5;
    printf("%d\n", *y);
  }

  int main() {
    uint32_t x = 10;
    f(&x, (uint16_t*)&x);
  }

Re: How to Think About Variables in C

#52

There are some subtle problems with the model as explained in this article. If you use this as your mental model, you will probably run afoul of undefined behavior without realizing it. If you read the C standard, you'll notice it doesn't talk much about "memory" (the word only appears 13 times in C99); it mostly talks about "objects" (mentioned 735 times in C99). These objects aren't OO-objects -- obviously C doesn'…

If a variable were just a memory address and assignment were just a memory copy, this would be a valid operation. It's a valid operation regardless of whether a standards body says it's not. uint32 x = 5; uint16 y = *(uint16*)&x; The effect is to set y to the first two bytes of memory from x. Values assigned to x are serialized into memory in either big endian or little endian order. Those are the only two cases you…

>int8, int16, int32, int64 are all explicit and force the compiler (and the hardware) to obey the wishes of the programmer.

At least in C99, the compiler doesn't need to support exact-width integer types.

>People make much ado about the fact that "a byte isn't necessarily 8 bits"

Well, POSIX.1-2004 requires that CHAR_BIT == 8.

Re: How to Think About Variables in C

#53

Earlier quoted context omitted.

No, it's really not. It's undefined behavior and the compiler is free to do absolutely whatever it wants. The point is that compilers do some specific thing, regardless of the fact that the standards bodies say they're free to reboot your computer. As long as all you care about is x86/x86_64/PowerPC (and probably ARM as well), then you can trust that the compiler is going to generate code which copies the first two b…

There be dragons. The following program prints 10 on gcc 4.6.3, x86-64: #include #include void f(uint32_t *x, uint16_t *y) { *x = 5; printf("%d\n", *y); } int main() { uint32_t x = 10; f(&x, (uint16_t*)&x); }

The antidote is to put a memory barrier in between the assignment and the printf.

  *x = 5;
  __sync_synchronize();
  printf("%d\n", *y);
http://gcc.gnu.org/onlinedocs/gcc-4.1.1/gcc/Atomic-Builtins....

The reason this example is fundamentally different from my example is because mine doesn't create two objects that point to the same memory. In such situations, memory barriers are necessary. Also, your program won't work on different platforms due to endianness.

Re: How to Think About Variables in C

#54
post #42

Earlier quoted context omitted.

The reason it's useful to explicitly "break the rules" like this is because it's important to know what assumptions you can in fact rely on, regardless of what standards bodies have to say about it. Given that compilers do break when programmers violate aliasing rules, you should recheck what assumptions you think you can rely on. Non-strict aliasing is not one of them. Unless you want to slow everything down with co…

uint8_t foo[4]; *(uint32_t*)foo = 0; Besides even without strict aliasing, the above is not at all guaranteed to work since not all architectures support unaligned loads. So, the interesting thing about this example is that it does work. It's in fact very, very difficult to find a platform where that example won't work (i.e. crashes the program). For example, any C library involving image manipulation is likely going…

> It's in fact very, very difficult to find a platform where that example won't work

No, it isn't. Many ARM processors will bus error on that code if (foo & 3) != 0. I believe PowerPC doesn't do unaligned word reads either...

It quite often has to do with the memory controller and not with the particular processor, though I believe x86 has to support unaligned reads. I've certainly worked first hand with ARMs that did not support it.

Re: How to Think About Variables in C

#55

What other mental models do people use to think about variables and memory? I would like to hear about them.

My understanding of types took a big step forward when I read some of Robert Harper's stuff. In particular, the blog post, Dynamic Languages are Static Languages, and his book, Practical Foundations for Programming Languages. (The book is a tome and I've only read parts of it but it's very good).

When it comes to understanding memory in C, another important aspect is understanding how linkers and loaders work. Also, it's good to know something about calling conventions.

Re: How to Think About Variables in C

#56

Earlier quoted context omitted.

There be dragons. The following program prints 10 on gcc 4.6.3, x86-64: #include #include void f(uint32_t *x, uint16_t *y) { *x = 5; printf("%d\n", *y); } int main() { uint32_t x = 10; f(&x, (uint16_t*)&x); }

The antidote is to put a memory barrier in between the assignment and the printf. *x = 5; __sync_synchronize(); printf("%d\n", *y); http://gcc.gnu.org/onlinedocs/gcc-4.1.1/gcc/Atomic-Builtins.... The reason this example is fundamentally different from my example is because mine doesn't create two objects that point to the same memory. In such situations, memory barriers are necessary. Also, your program won't work on…

That is not what memory barriers are for, at all. Memory barriers are a sequencing primitive for shared-memory concurrency (an excellent intro is here: http://lxr.linux.no/linux/Documentation/memory-barriers.txt). They are never required for correctness in valid single-threaded programs.

The memory barrier "fixed" this program similarly to how a cruise missile "fixes" a termite problem. It was just a coincidence and it was the wrong tool for the job.

Re: How to Think About Variables in C

#57
post #42

Earlier quoted context omitted.

The reason it's useful to explicitly "break the rules" like this is because it's important to know what assumptions you can in fact rely on, regardless of what standards bodies have to say about it. Given that compilers do break when programmers violate aliasing rules, you should recheck what assumptions you think you can rely on. Non-strict aliasing is not one of them. Unless you want to slow everything down with co…

uint8_t foo[4]; *(uint32_t*)foo = 0; Besides even without strict aliasing, the above is not at all guaranteed to work since not all architectures support unaligned loads. So, the interesting thing about this example is that it does work. It's in fact very, very difficult to find a platform where that example won't work (i.e. crashes the program). For example, any C library involving image manipulation is likely going…

any C library involving image manipulation is likely going to have code similar to what you've described

...which actually is exactly how I found out first-hand that it doesn't always work. If you only ever test on x86 you'll never catch it. You might not even catch it on ARM if you're lucky.

Which is the point - that compilers can and do make use of almost all undefined behavior of C for optimizations, which one developer might not catch because their current compiler happened to work. Then a new version is released that can find and exploit more undefined behavior. And strict aliasing is one of those rules you can't safely violate.

Re: How to Think About Variables in C

#58

Earlier quoted context omitted.

The antidote is to put a memory barrier in between the assignment and the printf. *x = 5; __sync_synchronize(); printf("%d\n", *y); http://gcc.gnu.org/onlinedocs/gcc-4.1.1/gcc/Atomic-Builtins.... The reason this example is fundamentally different from my example is because mine doesn't create two objects that point to the same memory. In such situations, memory barriers are necessary. Also, your program won't work on…

That is not what memory barriers are for, at all. Memory barriers are a sequencing primitive for shared-memory concurrency (an excellent intro is here: http://lxr.linux.no/linux/Documentation/memory-barriers.txt ). They are never required for correctness in valid single-threaded programs. The memory barrier "fixed" this program similarly to how a cruise missile "fixes" a termite problem. It was just a coincidence and…

Except we're talking about an invalid program. The program is invalid as written. Therefore memory barriers are the antidote because they're necessary in this situation.

A tool doesn't have a purpose. It has capabilities, and understanding why something works (and why it can be relied upon) is all that matters.

Re: How to Think About Variables in C

#59

Earlier quoted context omitted.

That is not what memory barriers are for, at all. Memory barriers are a sequencing primitive for shared-memory concurrency (an excellent intro is here: http://lxr.linux.no/linux/Documentation/memory-barriers.txt ). They are never required for correctness in valid single-threaded programs. The memory barrier "fixed" this program similarly to how a cruise missile "fixes" a termite problem. It was just a coincidence and…

Except we're talking about an invalid program. The program is invalid as written. Therefore memory barriers are the antidote because they're necessary in this situation. A tool doesn't have a purpose. It has capabilities, and understanding why something works (and why it can be relied upon) is all that matters.

Yes, it is an invalid program. The antidote is to fix it, not to jigger it in a way that happens to work. The memory barrier is not "necessary" -- it is not even a correct fix. Even with a memory barrier as you added it, it is still an invalid program that invokes undefined behavior. The memory barrier may have coincidentally fixed the problem on your system, but there is still no guarantee it will work on another architecture, another compiler, or even another version of the same compiler.

The problem with my program is that it casts an int32_t pointer to int16_t pointer. The correct fix is to not do that. "Fixing" the problem with a memory barrier is a step in the wrong direction.

Re: How to Think About Variables in C

#60

Earlier quoted context omitted.

Except we're talking about an invalid program. The program is invalid as written. Therefore memory barriers are the antidote because they're necessary in this situation. A tool doesn't have a purpose. It has capabilities, and understanding why something works (and why it can be relied upon) is all that matters.

Yes, it is an invalid program. The antidote is to fix it, not to jigger it in a way that happens to work. The memory barrier is not "necessary" -- it is not even a correct fix. Even with a memory barrier as you added it, it is still an invalid program that invokes undefined behavior. The memory barrier may have coincidentally fixed the problem on your system, but there is still no guarantee it will work on another ar…

but there is still no guarantee it will work on another architecture, another compiler, or even another version of the same compiler.

My point is that it is guaranteed to work. A memory barrier guarantees that all memory operations before the barrier take effect before any operations after the barrier.

I think this whole exchange is fascinating because it illustrates two completely different philosophies to hacking. Both are equally valid. I tend to prefer yours because it tends to result in shorter programs. Yet this is just a programmer convention. The machines do not care.

Yet there are some instances where my philosophy -- understanding which rules may be safely ignored -- has paid off. For example, if your invalid program were in a closed-source library which I was forced to interface with, then the program can't simply be fixed. In that case, a memory barrier would probably be the cleanest workaround.

It's an unfortunate fact that this type of situation -- broken third-party code that can't be fixed and can't be replaced -- is quite common in the field. It seems like it's an important skill for an engineer to know how to handle such situations.

EDIT: By the way, Scrybe Music looks really cool!

Post reply on HN