It’s not exclusively an IPv6 feature: RFC 3927 defines link-local IPv4 addresses, to be assigned randomly from 169.254.0.0/16 after a bit of ceremony to detect collisions.
Ideally, you’d be able to connect a PC and a printer with an Ethernet cable, they would both (having failed to find a better alternative) allocate a link-local address for themselves, and then the PC would use DNS-SD over mDNS to discover the printer and show it to you. Similar story with PCs exporting their media files over the network, a—say—set-top box, and a switch they’re all plugged into.
And for some combinations of parts this actually works. It’s just that the functionality is not always well-exposed by the OS, that a switch + DHCP server in a box (in practice, a consumer router) can work just as well with no configuration as an unmanaged switch can, and that people are not that interested in local-only wired networks anymore.
There’s also the “having failed to find a better alternative” part: unlike with IPv6, the RFC does not endorse always assigning a link-local address as the second one next to a static or DHCP-provided one, I’m guessing for software compatibility. Thus you really only see 169.254.* in your interface configuration when DHCP is borked, and it’s kind of useless in that case.
The IPv6 feature isn’t link-local addresses, it’s being able to specify the interface to bind to as part of the address specification. This lets you demand that your IPv6-based tool use your wired Ethernet connection, for example.
Yes, but the question is, "what if an address in this range is assigned to _two_ interfaces at the same time?" Now your local routing information base cannot distinguish which interface to use when trying to reach other hosts in that same network. So, it's fair to say, it's not a feature even available in IPv4.
The second difference is IPv6 is almost always going to have link local addresses assigned and machines with multiple network interfaces are the norm rather than the exception.
I literally had to interrogate an LLM to explain what this was about, because to me, indeed, when I see 169.254 I think "Ah, someone unplugged something critical and the network is now completely down." I didn't even know that in ipv6 land there are any reasons to use link-local addresses for anything. I mean, there still basically isn't a reason for 99.99% of people, I think. But it's interesting.
I also didn't realize that part of the idea behind these LL things was one of the rounds of wishful networking ideas of the 90s or 2000s, kind of a cousin of UPnP and mDNS in that way (in increasing order of eventual usefulness).
Considered completely in a vacuum, especially ignoring the WAN, I can see how it seemed silly that if you plugged three computers and a printer into a switch, rolling random IP addresses like this could have allowed things to be discoverable and to function locally (I thought mDNS or "Bonjour"/"Rendezvous" as Apple called it came much later, but I know my PCs could "see" each other with NetBIOS or whatever long before mDNS was invented).
Link-local addresses (LLAs) are needed in IPv6 because IPv6 doesn't have broadcast. IPv6 uses multicast instead.
Broadcasts go to all IPv4 addresses in the subnet, multicasts only go to those who subscribed to a multicast group. To subscribe to a IPv6 multicast group you need an IPv6 address. So all IPv6 interfaces will have at least one LLA self-generated.
One thing that IPv6 uses multicast heavily for is NDP, which is the IPv6 version of ARP. This is how IP addresses on your LAN/WLAN are converted to MAC addresses which is required info for the NIC in your node to talk to another node on your Ethernet LAN/WLAN.
End users don't typically have to use LLAs directly but you can use them if you want to 100% ensure things won't leave your LAN as routers don't forward LLAs.
mDNS on link-locals is what makes the "plug computers and printers into switch" case work. It would have been NetBIOS originally but mDNS is how it's done today.
Pedantically, yes VLAs are a mandatory part of C99 (only). Practically, there has been some resistance so they were reverted to optional in later standards (in C11/17, the whole thing is optional; in C23, variably modified array types are mandatory but the ability to allocate arrays of such types on the stack is not). In any case, MSVC is quite a bad benchmark as far as C (not C++) standards conformance goes—it’s been quite some time since Sutter’s (in)famous post[1] and things have improved, but not to the point that I’d believe C to be a priority for Microsoft.
(Note MSVC has alloca—and Microsoft’s own libraries in the past have done unwise things with it—so the safety argument for the lack of support does not fly.
I’ve thrown all my projects i have written in c for posix systems in the last 15 years against msvc and had only one project which had an issue. Because a dep used VLAs.
As far as I have been told, they were not made optional because of any issues with VLAs (which exist also in many other languages, including Ada) but because adoption of C99 was slow, so some parts were made optional in C11. Later VLAs were criticized for enabling stack clashing attacks that jump the guard page, but this also sometimes possible without VLAs and the real fix for this was compilers implementing stack probing. It is still bad when attacks control the size which should be avoided (and there is -Wvla-larger-than=)
> CPU processing of denormals tends to be extremely slow - I vaguely recall running into something like a 10x slowdown a decade ago
Intel CPU processing, where slowdowns can be as bad as couple hundred cycles. AMD CPUs penalize them much more mildly, usually single-digit cycles. (No idea about ARM.)
On the other hand, they (unexpectedly to the inventor, who intended them to be a debugging tool) underpin a few foundational results in correctly rounded computation, such as https://en.wikipedia.org/wiki/Sterbenz_lemma.
Not quite. As GP mentions, a[i][j] might mean either, depending on what the type of a is:
(a) If the type of a is “array of length N of pointer to (say) char” (declaration: char *a[N]), then a[i][j] means the jth char in the contiguous block pointed to by the ith pointer. In C#, this is what you get with an array of arrays.
(b) If the type of a is “array of length N of array of length M of char” (declaration: char a[N][M] — sic!), then a[i][j] means the jth element of the ith element, aka the (i*M+j)th char in the single contiguous memory block. In C#, this is what you get with a two-dimensional array.
The way this happens is a bit subtle:
(a) The value a, of type “array of size N of pointer to char”, first decays into “pointer to pointer to char”, then a[i] retrieves the ith “pointer to char” starting from it as a base, then in turn a[i][j] retrieves the jth “char” starting from that as a base.
(b) The value a, of type “array of length N of array of length M of char”, first decays into “pointer to array of length M of char” (sic!), then a[i] retrieves the ith “array of length M of char” starting from it as a base, which then decays into “pointer to char”, then a[i][j] retrieves the jth “char” starting from that as a base.
NB: There are no implicit references here, unlike in C#; in part (b), a is an N*M-byte chunk of memory and a[i] is an M-byte piece of it.
Funnily enough, Windows 98 is the first OS I remember with a sharing menu (“Send To”, which is memorable to me because the official Russian localization of it was suggestive of an obscenity). It seemed so pointless back then.
And it makes sense as long as you allow the concept of unsequenced operations at all (admittedly it’s somewhat rare; e.g. in Scheme such things are defined to still occur in sequence, but which specific sequence is unspecified and potentially different each time). The “volatile” annotation marks your variable as being an MMIO register or something of that nature, something that could change at any point for reasons outside of the compiler’s control. Naturally, this means all of the hazards of concurrent modification are potentially there.
That said, your “common parlance” definition of “data race” is not the definition used by the C standard, so your last sentence is at best misleading in a discussion of standard C.
> The execution of a program contains a data race if it contains two conflicting actions in different threads, at least one of which is not atomic, and neither happens before the other. Any such data race results in undefined behavior.
(Here “conflicting” and “happens before” are defined in the preceding text.)
Your first paragraph makes it sound as if the compiler will actually generate two reads of the value of some register, which might lead to unexpected effects at runtime for certain special registers.
However, this is not at all what UB means in C (or C++). The compiler is free to optimize away the entire block of code where this printf() sequence occurs, by the logic that it would be UB if the program were to ever reach it.
For example, the following program:
int y = rand();
if (y != 8) {
volatile int x;
printf("%d: %d", x, x) ;
} else {
printf("y is 8");
}
Can be optimized to always print "y is 8" by a perfectly standard compliant compiler.
> Your first paragraph makes it sound as if the compiler will actually generate two reads of the value of some register, which might lead to unexpected effects at runtime for certain special registers.
I don’t see how. I was trying to explain why it’s reasonable for a volatile read to be a side effect, after which the C rule on unsequenced side effects applies, yielding UB as you say.
"volatile" tells the compiler it is _not_ safe to optimise away any read or write, so it can't just optimise that section away at all.
> An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects. Therefore any expression referring to such an object shall be evaluated strictly according to the rules of the abstract machine, as described in 5.1.2.3. Furthermore, at every sequence point the value last stored in the object shall agree with that prescribed by the abstract machine, except as modified by the unknown factors mentioned previously.
A compliant compiler is only free to optimise away, where it can determine there are no side-effects. But volatile in 5.1.2.3 has:
> Accessing a volatile object, modifying an object, modifying a file, or calling a function that does any of those operations are all side effects.
Yes, but undefined behaviour is undefined behaviour, and that behaviour can legally be that the code is not emitted at all, volatile (or any other side effect) or not. (and compilers do reason about undefined behaviour when optimising, so this isn't necessarily a completely theoretical argument, though I don't know whether the in compiler's actual logic which of 'don't optimise volatile' or the 'do assume undefined behaviour is impossible and remove code that definitely invokes it' would 'win', or whether there's any current compiler that would flag this as unconditionally undefined behaviour in the first place).
GCC calls that out [0] - volatile means things in memory may not be what they appear to be, and that there are asynchronous things happening, so something that may not appear to be possible, may become so, because volatile is a side-effect.
So about the only optimisation allowed to happen, is combining multiple references.
Clang is similar:
> The compiler does not optimize out any accesses to variables declared volatile. The number of volatile reads and writes will be exactly as they appear in the C/C++ code, no more and no less and in the same order.
This is all assuming that the code is not invoking undefined behaviour. If the code is invoking undefined behaviour, GCC and clang are both well within their rights to say 'none of the rest of our documentation applies' (and have historically done so on bug reports).
I've mentioned elsewhere the standards, and compilers as well, disagreeing with you here.
But feel free to run against the various compilers through godbolt. [0] They won't optimise the branch away. Access to a volatile, must be preserved, in the order that they exist. No optimisation, UB or otherwise, is allowed to impede that. Because an access is a side-effect.
> Furthermore, at every sequence point the value last stored in the object shall agree with that prescribed by the abstract machine, except as modified by the unknown factors mentioned previously.
I quoted the C standard, first. Not compiler behaviour.
I showed where it requires the compiler not to optimise this.
How about, instead of one-line throwaway disagreements, you point out where they are permitted to do this, instead?
The compiler is required to not optimise out reads/writes through volatile. That's unrelated to code also having UB: you can't sprinkle volatile through arbitrary UB and suddenly have it be defined.
> A compliant compiler is only free to optimise away, where it can determine there are no side-effects
A compliant compiler is also allowed to assume UB cannot occur.
> that can easily be solved by a minute or two on godbolt...
Unfortunately it's not that simple when it comes to UB. If the snippet in question does in fact exhibit UB then there's no guarantee whatever Godbolt shows will generalize to other programs/versions/compilers/environments/etc.
Also, at behavioural edges what you'll see on Godbolt is compiler bugs. So you learn nothing about what should happen.
All popular modern C++ compilers have known bugs and while I'm sure there are C compilers with no known bugs that will be because nobody tested very hard.
No, claim A is 'x may be removed by a conforming C compiler'. Whether any given version of a given compiler actually does so in any given circumstance is a different question (the answer being: probably not, because while this is undefined behaviour it's not likely something that is going to be flagged as such by a compiler's optimizer. Also, from some testing with GCC and forcing a null point dereference, it seems like volatile at least does win in that case with the current version of it x86, and it dutifully emits the null pointer dereference and then the 'ud2' instruction instead of the rest of that execution path).
I made the weaker claim that x can be removed. This is something I could prove with compiler output but I would have to find a compiler willing to make this optimization which is not something I can guarantee.
When compiler decides something is UB aka "result of this code is not defined and could be any" it selects the most performant version of undefined behavior - doing nothing by optimizing code away.
The compiler is not free to remove accesses to something marked volatile - its defined as a side-effect.
Volatile means something else may be acting here. Something else may install anything into the register at any time - and every time you access.
The compiler is required to preserve the order of accesses. In almost every C compiler, today, there are almost no optimisations the moment a volatile is introduced, for this reason.
If code has undefined behavior, the entire execution path that leads to that UB has no assigned semantics in the C model. So there are no volatile accesses in this code according to the C abstract machine - the entire execution path is UB, so it can be assumed it doesn't happen at all.
> An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects. Therefore any expression referring to such an object shall be evaluated strictly according to the rules of the abstract machine
The execution path has unknown side effects, and so the execution path must be strictly followed. That's uh... The entire point of that section in the C standard. Its why volatile is called out, in the semantic model for the abstract machine.
Otherwise... Why call it out, at all? It must be strictly followed, not lazily, as in other areas of the standard.
UB supersedes volatile, once the compiler hits UB then all bets are off. Compilers can and do optimize out UB branches, which is almost never what you want... yet here we are.
>> The moment you enter a compilation unit (assuming no link optimizations) with a state which at some point will run into undefined behavior all bets are of. [...] Yes, UB can "time travel"
> Close, but not quite. This is a common misconception in the reverse direction.
> Abstractly, what UB can do is performing the inverse of the preceding instructions, effectively making the abstract machine run in reverse. However, this is only equivalent to "time-traveling" until you get to the point of the last side effect (where "side effect" here refers to predefined operations in the standard that interact with the external world, such as I/O and volatile accesses), because only everything since that point can be optimized away under the as-if rule without altering the externally visible effects of the program.
> As a concrete, practical example, this means the following: if you do fflush(stdout); return INT_MAX + 1; the compiler cannot omit the fflush() call merely because the subsequent statement had undefined behavior. That is, the UB cannot time-travel to before the flush. What the program can do is to write garbage to the file afterward, or attempt to overwrite what you wrote in the file to revert it to its previous state, but the fflush() must still occur before anything wild happens. If nobody observes the in-between state, then the end result can look like time-travel, but if the system blocks on fflush() and the user terminates the program while it's blocked, there is no opportunity for UB.
The print example has no defined order of accesses, function parameters can be evaluated in any order. But further, the entire problem with UB is that it supercedes the regular guarantees that you get (like with volatile) when it's encountered. Yes gcc and clang do the obvious thing that makes the most sense in this example, but what people are trying to tell you is that they could just not do that and they would still be complying with the standard. For example, you can imagine a more serious example of UB that causes the program to fail to compile completely, and then do you emit the correct number of in order reads of volatile variables? Obviously not.
Function parameters cannot be evaluated in any order, when one of them is a volatile.
> The initialization shall occur in initializer list order, each initializer provided for a particular subobject overriding any previously listed initializer for the same subobject
And what I am trying to tell people, is the standard has expectations around the volatile keyword, that the compilers took into account when designing how they would work - it isn't just kindness, its compliance. But no one is actually talking about the quotes from the standard, and just quoting themselves and their own understandings.
That quote doesn't have anything to do with parameter evaluation order. There is no order for function parameter evaluation.
And no, there is no exception for undefined behavior. There can't be, otherwise the behavior would be... defined. It's in the name. Again, what do you think the compiler emits when the undefined behavior causes the program to not compile altogether?
Not from the standard’s point of view. The traditional (in some circles) use of volatile for atomic variables was not sanctioned by the C11/C++11 thread model; if you want an atomic, write atomic, not volatile, or be aware of your dependency on a compiler (like MSVC) that explicitly amends the language definition so as to allow cross-thread access to volatile variables.
Ideally, you’d be able to connect a PC and a printer with an Ethernet cable, they would both (having failed to find a better alternative) allocate a link-local address for themselves, and then the PC would use DNS-SD over mDNS to discover the printer and show it to you. Similar story with PCs exporting their media files over the network, a—say—set-top box, and a switch they’re all plugged into.
And for some combinations of parts this actually works. It’s just that the functionality is not always well-exposed by the OS, that a switch + DHCP server in a box (in practice, a consumer router) can work just as well with no configuration as an unmanaged switch can, and that people are not that interested in local-only wired networks anymore.
There’s also the “having failed to find a better alternative” part: unlike with IPv6, the RFC does not endorse always assigning a link-local address as the second one next to a static or DHCP-provided one, I’m guessing for software compatibility. Thus you really only see 169.254.* in your interface configuration when DHCP is borked, and it’s kind of useless in that case.
reply