3

Let's say we have the following code

#include <stdlib.h>
#include <limits.h>
#include <stdio.h>

int main(void) {
    char *w = malloc(sizeof(long));
    long *n;

    *w = 'x';
    printf("some character: %c\n", *w);
    n = (long *)w;
    *n = LONG_MAX;
    printf("LONG_MAX: %ld", *n);

    return 0;
}

on a system where long is more strictly aligned than char (which will be most systems).

If we didn't print the long object *n, the result of malloc would only be used for a char (which has alignment 1). That is, it seems that an intelligent compiler wouldn't have to be bound by the constraint that the result of malloc be compatible with any object type (see this question: how does malloc understand alignment?); it could thus have malloc return an arbitrarily aligned slice of memory.

With the additional lines of code that deal with n, such a compiler wouldn't be able to do that anymore, since the result of malloc now also has to be suitable for a long. As far as I understand, the above code doesn't violate alignment requirements or strict aliasing rules.

Is my understanding correct?

  • Can a compiler optimize calls to malloc to return more weakly aligned memory if it knows what the memory is used for?
  • Similarly, is the compiled code allowed to rearrange memory behind the scenes to weaker alignment, as long as there are no alignment violations? That is: In the above case, if we omitted all lines dealing with n, would it be legal for the machine to change w to an arbitrarily aligned address immediately after the line char *w = malloc(sizeof(long));?

The opposite behavior would be that once memory has been assigned by malloc, it has to keep its maximally compatible alignment.

Lundin
  • 195,001
  • 40
  • 254
  • 396
Lover of Structure
  • 1,561
  • 3
  • 11
  • 27
  • Note: This time I wasn't sure whether to add the `language-lawyer` tag, but anyone please add it (or let me know) if you think it is appropriate. – Lover of Structure Jul 04 '23 at 07:59
  • 3
    There is as-if rule. Compiler can cause world war 3 and cure cancer, as long as the output of the program is correct. – KamilCuk Jul 04 '23 at 08:13
  • On success, `malloc` returns a pointer that can be assigned to a pointer to any type of object with a fundamental alignment requirement (Ref: C17 7.22.3/1). That should be the case even when the allocated size is less than the the largest fundamental alignment size. `long` has a fundamental requirement alignment, therefore the pointer value will be suitable for assignment to a `long *`. – Ian Abbott Jul 04 '23 at 08:53
  • 2
    @IanAbbott: Those rules only apply to the "abstract machine". As long as the observable behavior does not change, the compiler is not required to actually abide by these rules. For example, in this case, it can optimize away the entire call to `malloc`. See [§5.1.2.3 of the ISO C11 standard](http://port70.net/~nsz/c/c11/n1570.html#5.1.2.3) for further information. – Andreas Wenzel Jul 04 '23 at 09:14
  • @AndreasWenzel I am more concerned with the general case, but indeed for the example code, all the code needs to do is send a predetermined character sequence to standard output. – Ian Abbott Jul 04 '23 at 09:16
  • 1
    @AndreasWenzel But we could force the compiler to generate the call to `malloc` using `char *w = (void*(*volatile)(size_t)){malloc}(sizeof(long));` and then the returned pointer must be suitably aligned for assignment to a `long *`. Although the compiler is under no obligation to actually use the returned pointer in the remaining code, as per the as-if rule. – Ian Abbott Jul 04 '23 at 09:42
  • 3
    Language lawyer is very much appropriate since this happens to be a broken part of the C language. The wording has changed several times from C99 to C23, the latest defect fix being [N2293](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2293.htm) which made it into C23. – Lundin Jul 04 '23 at 11:13
  • 2
    A slightly different but interesting question is: [what is the alignment requirement of `malloc(1)`](https://stackoverflow.com/questions/76618849/what-is-the-alignment-requirement-of-malloc1) – chqrlie Jul 05 '23 at 09:19

3 Answers3

4

Is my understanding correct?

You stated assumptions and then you stated result. Yes, if the compiler optimizes this malloc with memory that is not correctly aligned to the representation of the long datatype on a particular platform and will generate an instruction to access that memory that is not "able" to access that unaligned memory, then some odd implementation behavior may happen. You would have to check the actual machine code generated by the compiler and documentation of the machine you are using.

This is all about code generation from the compiler. Compiler can't "break alignment rules of C data types". The compiler generates machine code. You feed the compiler with "correct" C code that does not break alignment. If you executed your code on an "abstract machine where optimization issues are irrelevant" (5.1.2.3p1), then the behavior of the code has to be sane. The compiler has to generate (any) such machine code, that behaves "as described" by the C code - produces the correct output (side effects).

Can a compiler optimize calls to malloc to return more weakly aligned memory if it knows what the memory is used for?

Sure.

is the compiled code allowed to rearrange memory behind the scenes to weaker alignment, as long as there are no alignment violations?

Yes, but compiler does not care about "C alignment violations", compiler cares about machine code that it generates and if that machine code will break any implementation specific (not C language specific) constraints about the machine.

The compiler can do anything as long as the output is the same. On x86 you can (with general instructions) access unaligned word without problems, just slower. So on x86 even if the compiler replaces malloc with memory aligned to byte, you will not notice, and the compiler will be correct - the code output is as intended, all fine. It's a quality of implementation concern.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111
4

As far as I understand, the above code doesn't violate alignment requirements or strict aliasing rules.

Is my understanding correct?

Yes, the dynamically allocated memory is guaranteed to have alignment sufficient for long, and the standard permits using dynamically allocated memory flexibly; storing to dynamically allocated memory with a new non-character object type changes its effective type to be the type used for the store.

We can analyze the requirements of the standard in three steps.

A. What behavior the standard specifies

The C standard specifies the behavior of a program in an abstract machine. In this abstract machine:

  • char *w = malloc(sizeof(long)); allocates memory suitably aligned for a long in all C standards from C 1999 to the prospective C 2023 (possibly more restrictively aligned in some versions of the standard). For purposes of discussion, we assume the malloc succeeds.
  • *w = 'x'; writes the character “x” into the memory.
  • printf("some character: %c\n", *w); prints the message with the character “x”.
  • n = (long *)w; converts the address to long *. This is defined by C 2018 6.3.2.3 7, which supports converting between pointers to object types provided the address is suitably aligned, which we know it is.
  • *n = LONG_MAX; writes LONG_MAX into the memory. This does not alias the memory as a different type because C 6.5 6 says the effective type for dynamically allocated memory becomes the type used to store to it, so the memory is treated as having type long for this access.
  • printf("LONG_MAX: %ld", *n); accesses the memory with type long, which is the current effective type of the memory, so this prints the message with the LONG_MAX value.

B. Observable behavior

C 2018 5.1.2.3 says a conforming C implementation is only required to produce the observable behavior, and the observable behavior is:

  • Accesses to volatile objects are evaluated strictly according to the rules of the abstract machine.
  • At program termination, all data written into files shall be identical to the result that execution of the program according to the abstract semantics would have produced.
  • The input and output dynamics of interactive devices shall take place as specified in 7.21.3. The intent of these requirements is that unbuffered or line-buffered output appear as soon as possible, to ensure that prompting messages actually appear prior to a program waiting for input.

So the observable behavior of the code is:

  • It prints “some character: x” followed by a new-line character.
  • It prints “LONG_MAX: ” followed by the value of LONG_MAX in decimal and a new-line character.

C. Apply the above to answer the questions

Can a compiler optimize calls to malloc to return more weakly aligned memory if it knows what the memory is used for?

If the compiler produces a program that:

  • Prints “some character: x” followed by a new-line character.
  • Prints “LONG_MAX: ” followed by the value of LONG_MAX in decimal and a new-line character.

then it has satisfied the requirements of the C standard. So, if the compiler produces a program that uses more weakly aligned memory than it might be abstractly required to in C 2018 and the program prints those messages, then it has conformed to the C standard. For example, if the fundamental alignment requirement is 16 bytes, then the malloc in the abstract machine is required to produce an address aligned to a multiple of 16 bytes. However, that is not observable behavior, so the actual program produced by the compiler is not required to do that. As long as it prints the required messages, it conforms.

Similarly, is the compiled code allowed to rearrange memory behind the scenes to weaker alignment, as long as there are no alignment violations? That is: In the above case, if we omitted all lines dealing with n, would it be legal for the machine to change w to an arbitrarily aligned address immediately after the line char *w = malloc(sizeof(long));?

If the n lines were removed, the observable behavior of the program would be:

  • Print “some character: x” followed by a new-line character.

As long as the compiler produced a program that printed that message, the alignment of any memory used to do that would be irrelevant to whether it conformed to the C standard or not. All the C standard requires is that that message be printed.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • A slightly different but interesting question is: [what is the alignment requirement of `malloc(1)`](https://stackoverflow.com/questions/76618849/what-is-the-alignment-requirement-of-malloc1) – chqrlie Jul 05 '23 at 09:18
0

First off, writing memory as one type and reading it back as another type falls foul of the anti-aliasing rule. GCC may optimize the write away. (probably mistaken in this)

Second, malloc is a library function and what it does and how it works is not something that the compiler knows about. Malloc itself does not specifically align memory (although probably will for larger allocations), if you want alignment, you should use memalign

Note: malloc is a recognized builtin in gcc so gcc does have some awareness of what it does but this is only to avoid calling malloc entirely for trivial cases.

doron
  • 27,972
  • 12
  • 65
  • 103
  • 1
    Regarding strict aliasing: I thought my code is technically not in violation because one of the types in question is `char *`. – Lover of Structure Jul 04 '23 at 09:11
  • @LoverofStructure As I understand it, in c you can legally assign any pointer to char* but not necessarily the other way round like in your program, n = (long *)w; – Simon Goater Jul 04 '23 at 09:20
  • @SimonGoater That *is* a good point – but what if we simply pretend that it's `long` memory initially accessed as a `char` or alternatively replace the first 2 lines of `main` by `long *n = malloc(sizeof(long)); char *w = n;`? – Lover of Structure Jul 04 '23 at 09:26
  • @LoverofStructure Yes, that would fix the aliasing issue. However, there's no legal way the compiler can return a pointer to memory with alignment which wouldn't be good for a long, so there's no room for any optimisation there. I guess if you're really trying to conserve memory, you could write a custom mymalloc() function that malloc'ed char* memory and then just returned pointers to sections of that. – Simon Goater Jul 04 '23 at 09:37
  • 2
    @LoverofStructure: You are correct that the strict aliasing rule is not violated, for two reasons: (1). Sentence #2 of [§6.5 ¶6 of the ISO C11 standard](http://port70.net/~nsz/c/c11/n1570.html#6.5p6) does not apply, because `*w` has "character type". (2). Even if sentence #2 hypothetically did also apply to "character types", it would not matter, because that would only set the "effective type" of the object to `char` until the object is modified as a `long` in the line `*n = LONG_MAX;`. [continued in next comment] – Andreas Wenzel Jul 04 '23 at 09:51
  • 2
    @LoverofStructure: [continued from previous comment] Before this modification takes place, no attempt is made to read the object as a type other than its "effective type" `char`. The line `*n = LONG_MAX;` gives the object pointed to by `n` and `w` the new effective type `long`. Afterwards, the object is only read as a `long`, so that is ok, too. – Andreas Wenzel Jul 04 '23 at 09:52
  • @SimonGoater: You are correct that you can read any object through a pointer to `char`, but not necessarily the other way around. For example `char buffer[100]={0}; int *p = (int*)buffer; *p = 20;` would violate the strict aliasing rule, because the object pointed to by `p` has the "effective type" `char`. However, that is because the "declared type" of the object is `char`. The object returned by `malloc` does not have a "declared type", so the "effective type" of the object returned by `malloc` can never be `char`. See my posted link to the ISO C standard for further information. – Andreas Wenzel Jul 04 '23 at 10:12
  • @AndreasWenzel Right – because `'x'` and `LONG_MAX` are different objects, not the same object accessed differently. – Lover of Structure Jul 04 '23 at 10:16
  • 1
    @LoverofStructure: Even if `'x'` and `LONG_MAX` were the same `int` or `long` object, it would not make any difference. When assigning the object to `*w`, that object would get converted to `char`, and when assigning that object to `*n`, that object would get converted to `long`. However, `'x'` and `LONG_MAX` are not "objects", they are simply "values". – Andreas Wenzel Jul 04 '23 at 10:23
  • @LoverofStructure: [§3.15 of the ISO C standard](http://port70.net/~nsz/c/c11/n1570.html#3.15) defines an "object" as a "region of data storage [...], the contents of which can represent values." Therefore, in my opinion, `'x'` and `LONG_MAX` are `int` and `long` values, but they are not "objects". As far as I can tell, the term "object" can only be applied to lvalues, not rvalues. However, I am not sure if this terminology is also used in the programming language C++. If I am not mistaken, any instance of a class is called an "object" in C++, even if it is not an lvalue. – Andreas Wenzel Jul 04 '23 at 10:37
  • @AndreasWenzel I was indeed a bit unsure when writing the comment above yours. I first wanted to write something like "the objects `'x'` accessed via `w` and `LONG_MAX` accessed via `n`". But now I realize that I'd have to study the standard in more depth to be able to say something whose accuracy I'd be certain of ... So I'll leave things as they are – and, thanks for all the valuable comments! – Lover of Structure Jul 04 '23 at 10:43
  • 2
    @AndreasWenzel Your reasoning isn't correct. 6.5 §6 2nd sentence says "If a value is stored into an object having no declared type through an lvalue having a type that is **not a character type**" and that does not apply because `*w = 'x';` is an access through a lvalue that _has_ character type. The next sentence is what applies, the effective type becomes `char`. And therefore `*n = LONG_MAX;` is a strict aliasing violation because it has (6.5 §7) "its stored value accessed only by an lvalue expression that has one of the following types" and `long` is not one of the mentioned types. – Lundin Jul 04 '23 at 11:04
  • 2
    @Lundin: In `*n = LONG_MAX;`, `n` has type `long *`, so `*n` has type `long`, and C 2018 6.5 6 applies: “If a value is stored into an object having no declared type through an lvalue having a type that is not a character type, then the type of the lvalue becomes the effective type of the object for **that access** and for subsequent accesses that do not modify the stored value.” So, for this assignment, the effective type of the memory is `long`, and therefore accessing it with a `long` value conforms to the aliasing rules in 6.5 7. – Eric Postpischil Jul 04 '23 at 12:03
  • 2
    @doron: Re “First off, writing memory as one type and reading it back as another type falls foul of the anti-aliasing rule.” The code in the question does not do this. First `*w = 'x';` writes it with a `char` lvalue. Then `printf(…, *w);` reads it with a `char` lvalue. Then `*n = LONG_MAX;` writes it with a `long` lvalue. Then `printf(…, *n);` reads it with a `long` lvalue. Each read uses the type of the most recent write, so there is no writing memory as one type and reading it as another. – Eric Postpischil Jul 04 '23 at 12:06
  • @EricPostpischil You skipped `n = (long *)w;`, so `n` refers to memory with an effective type of `char`. Writing a `long` to `char` memory is a strict-aliasing violation. Laundering the `char *` pointer through a `long *` pointer does not change the effective type of the actual memory - if it did, there'd be no strict-aliasing rule to be violated. – Andrew Henle Jul 04 '23 at 15:35
  • @AndrewHenle: `n = (long *)w;` does not set `n` to point to memory with an effective type of `char`. `char *w = malloc(sizeof(long));` sets `w` to point to dynamically allocated memory, which initially has no effective type. `*w = 'x';` accesses it via a character type, and it is not copying from any object, so it falls into the last sentence of 6.5 6: “For all other accesses to an object having no declared type, the effective type of the object is simply the type of the lvalue used for the access.” This sentence gives the effective type for that access but not for any future accesses… – Eric Postpischil Jul 04 '23 at 15:38
  • … so the object still has no effective type for future accesses. `n = (long *)w;` does not access the memory and has no effect on its effective type. Then `*n = LONG_MAX;` accesses it with type `long`, and 6.5 6 says the effective type for this access and future accesses is `long`. That is actually fundamental to using dynamically allocated memory: Storing into it with any non-character type gives it that effective type. You can write a `float` into dynamically allocated memory and use it and then write a `long` into it and use it for that. A write of any type is okay and gives it a new type. – Eric Postpischil Jul 04 '23 at 15:41
  • @AndrewHenle Irrespective of who is factually right, I do approve of your novel use of the verb "to launder". They should incorporate such usage into the C standard. – Lover of Structure Jul 04 '23 at 15:53
  • @EricPostpischil *`*w = 'x';` accesses it via a character type, and it is not copying from any object* `'x'` is not an object? Or are you referring to use of `memcopy()`? – Andrew Henle Jul 04 '23 at 16:19
  • @AndrewHenle: `'x'` is not an object. It is a character constant and is a value with type `int`. An object is a region of memory that can represent values, and `'x'` is not a region of memory and does not occupy any region of memory in the computing model of the C standard; there is no lvalue for it. The reason for rule about effective type via copying bytes is so that, if the compiler sees code copying from, say, a `float` into dynamically allocated memory at `p`, it can figure, for optimization, there is a `float` there and some other `int *q` does not point to the same memory. – Eric Postpischil Jul 04 '23 at 16:25