last day (2 days later) » 

19:15
0
A: How do modern compilers choose which variables to put in registers?

supercatWhen used with -O0, gcc ARM will keep things declared with register qualifier in registers if enough registers exist. While the qualifier may be viewed as obsolescent, this strategy can sometimes yield better results than fancier approaches. For example, when targeting the Cortex-M0 (use -mcpu=...

That GCC codegen does indeed seem remarkably poor, which is curious. Clang does much better on this example (though e needs to be initialized to avoid UB). I don’t know why GCC’s codegen is so poor here, but I know much less about GCC’s internals in general. Regardless, I’m not sure what you mean by “clang can be coaxed into generating better code”, seeing as it generates essentially the code you’ve written at -O1 and above.
@AlexisKing: Oops; I thought I'd fixed the code to properly say `e=p+n*4. Optimal code, if items are supposed to be processed in order, would be to scale n up by 16, displace p by n bytes, invert n, and then use indexed addressing, so that n can count up to zero. It's tough to prevent clang from trying to use a separate marching pointer and counter, though. My main point with the example was to show that compilers exist which simply use registers for things declared as registers, separate from other logic to try to infer what should be in registers.
Ah, I see what you mean; I missed what you were saying. That makes sense, though—automatic transformation of loops and loop nests is a surprisingly hard problem. The space of possibilities is large. (The failure of automatic vectorization is really just one instance of that phenomenon.)
@AlexisKing: A key difference between Fortran and C is that the former was designed around the assumption that language constructs should allow compilers to understand what's going on well enough to perform such transforms, while C was designed to allow programmers to perform such transforms so compilres wouldn't have to. A massive underappreciated tragedy is that in the 1980s, people who wanted a high-end computed language that didn't silently ignore everything past column 72 took over the C Standards Committee rather than working to expedite the next Fortran standard.
@AlexisKing: The vast majority of heated controversies surrounding C are fundamentally arguments over whether C is supposed to be a low-level systems programming language or a Fortran replacement, ignoring the fact that the drawing a clear distinction between the two kinds of tasks would allow both to be accomplished more cheaply, easily, and effectively, than trying to have language constructs serve both purposes without distinguishing between them.
I agree with you; I think Fortran makes substantially more sense than C as a programming language in the contemporary computing environment, for numerous reasons, including the ones that you mention. See also this discussion I just had with someone on this subject on bsky.
19:15
@AlexisKing: I disagree with the premise of your first post; some tasks require a language that uses the same abstraction model as the ABI via which high-level assemblies would use to interoperate with each other on a given platform, and C was designed to be such a language, and is for most tasks that require that abstraction model dialects of C that use that abstraction model are better than any alternative. Attempts to make C more suitable for high-end computing on modern platforms are analogous to attempts to make a chain saw more suitable for high-volume cutting by adding...
...an automatic material feeder. Chain saws and table saws are designed for different kinds of jobs; if improvements in table saws reduce the number of jobs where chain saws would be a best choice, fans of chain saws shouldn't respond by trying to make them more suitable for such jobs, but instead maximize their suitability for jobs for which no better alternatives exist. It irks me that silicon vendors' free tools are designed around C compilers whose maintainers view low-level programming idioms as warts in the language, rather than the most uniquely useful feature.
@supercat I essentially agree with you about this. If you are doing ABI things, it was once possible to use C to write code that utilized the hardware’s memory model. It is no longer possible to do that, and there are essentially no programming languages in which it is possible to do that (ignoring, say, LLVM IR, which is not really intended to be written by humans).
I think the reason C has gone in this direction is that, in practice, lots of people have wanted to write computer programs in C, most of those programs do not rely on access to the hardware’s memory model, and programmers want their programs to go fast. It was therefore expedient to specify C as a PL, with its own semantics/abstract machine, in a way that allowed the large corpus of C code to be efficiently executed on hardware increasingly removed from programmers’ mental models.
I do essentially agree with you that this was perhaps a poor decision. C is not terribly well-suited to being a programming language because it specifies very little structure that would be useful for optimizing compilers to utilize. Its constructs were designed to have a one-to-one correspondence to operations on memory at the hardware’s level of abstraction, and that is no longer the case.
I think the trouble is that preserving that original semantics is somewhat challenging in a modern context simply because any code that relies on having access to the hardware’s memory model also invariably requires some logic written around it that does not need that access, and specifying it so that the one-to-one correspondence is always preserved results in poor performance for no reason.
It does feel like perhaps a better solution would be to have something similar to Rust’s unsafe blocks, where a region of code can be switched into a mode in which language constructs have that direct interpretation at the hardware’s level of abstraction, with suitable rules for gluing those regions into the rest of the program. It is unfortunate that no such language exists. But I think that is partly because the demand for it is fairly low.
19:31
@AlexisKing On many embedded systems, compilers do still use that model except when optimizations break things.
Yes, and I will happily agree that modern C is clearly not serving embedded systems very well!
@AlexisKing: Further, even on desktop systems, C would need very few tweaks to accomplish most OS-related tasks in toolset-agnostic fashion.
Sure, it would just compromise the overwhelmingly common use case that modern C is used for, which is writing computer programs that do not need to perform ABI-level tasks.
I’m not saying I think it is good to prioritize those use cases, but those are the decisions that have been made.
For example, a construct which says "generate a machine-code function with the indicated signature, which is ___-byte aligned and contains the following bytes" would be sufficient to accomplish most of the tasks which wouldn't otherwise map to C constructs.
You’d also have to specify some way to safely call such a function from the rest of the C. Which I mean, yeah, you could totally do. It might even be useful! It definitely would for some use cases. But I don’t think there are people going to bat for that stuff on the standards committee. Just look at what happened with bitfields.
19:38
@AlexisKing For what fraction of the devices that run C code is the supposedly "overwhelmingly common" use case applicable?
I think it’s less about the fraction of the devices and more about the fraction of the lines of C code written.
And look, again, I am not trying to defend C here. I hate C! I would prefer that none of these people used it for this purpose and that you could have it back. But it’s become a kind of lingua franca for workstation operating systems in a way that makes it more or less impossible to get rid of now.
@AlexisKing Most platforms have a specified ABI. If a compiler would know how to invoke a function defined in some other compilation unit that it knows nothing about beyond the signature, the binary blob would be coded to expect registers to be set up the same way.
I agree, it could be done. I’m just saying that there are not enough people going to bat for this for the people who work on the language and compilers to implement it. But if a large enough group of people made a big push for this, I do not think it’d be fundamentally impossible to get implemented somewhere.
@AlexisKing The exremely vast majority of lines of C code would work equally well under both abstraction models.
Sure, but most of them would be a lot slower.
19:42
@AlexisKing One thing that's needed is a retronym to distinguish low-level C from Fortran-wannabe C.
@AlexisKing Only a tiny fraction of them would be slower by a meaningful amount.
I think most C is not doing Fortran stuff, to be honest. The HPC people are largely actually writing Fortran or C++ (or something else entirely). Most C is used for glue code, OS utilities, and various low-level systems programming tasks, in part because C is the exposed API for workstation operating systems.
@supercat I am not sure you have the evidence to support this argument.
Further, if one treats operations involving automatic-duration objects whose address is not taken as "non-observable", the performance difference would for many tasks be minimal or non-existent.
Now, admittedly, I actually think a lot of C optimizations really are probably not worth the effort, when it comes down to C specifically. But of course C compilers are also C++/Rust compilers, and people really do rely on the performance characteristics there.
C is a weird language with a very odd coalition of users for social reasons more than technical ones. I think you and I largely agree on most of these points. I think the performance of a lot of C code would not be significantly hurt by just compiling with -O0, honestly, because it’s largely glue. But the big, hulking optimizing compilers really mostly serve C++, not C. C is just along for the ride.
@AlexisKing Most programs spend the vast majority of their time executing only a small portion of their code. If only 9% of a program's execution time was spent in the least-used 50% of it, increasing by an order of magnitude the execution time of all of those lines would only double execution time.
Sure, but people often drop down to C specifically to write a hot loop in a program that is otherwise implemented in some other language!
19:53
Such loops represent only a small minority of the corpus of C code.
I don’t disagree, but people have come to rely on it nevertheless. And as I said, the people building these optimizations are mostly focused on C++/Rust, where it does matter.
Like, sure, you could write a C compiler that doesn’t do these things. Nobody is stopping anyone from doing that!
If one looks at the code generated at -O0, it's pretty terrible, granted. On the other hand, an ARM compiler that did a good job of applying 1990s optimization principles and did nothing else would likely generate better code than what clang and gcc generate in practice.
GCC codegen is often weird and bad. I cannot argue with that. But in my experience, Clang does a pretty good job at not being stupid, at the very least. I have a hard time believing that 1990s era compiler tech would produce better programs on non-embedded hardware. On embedded hardware, sure, but those are fundamentally different machines and not really the systems targeted by Clang and GCC. They’re supported as a nicety.
@AlexisKing Silicon vendors look at the support infrastructure around gcc, and the fact that it mostly seems to work, and view that as more worth investing in than anything a lone programmer would be able to produce.
Certainly, but that’s sort of my point: these things are social problems more than they are technical ones. There is not the social will to produce a C compiler that is not LLVM/GCC. Nobody is really in disagreement over the technical merits, I think, it’s just a question of who cares enough to do (and possibly fund) the work.
20:00
void test2(int *arr, int n)
{
    n*=16;
    while((n-=16) >= 0)
    {
        *(int*)((char*)n) += 0x12345678;
    }
}
Essentially the same as the earlier function, but counting down. Should be easy to turn into a five-instruction loop.
What does clang produce?
.LBB0_2:
    mov     r2, r0
    subs    r2, #32
    ldr     r3, [r2]
    adds    r3, r3, r1
    str     r3, [r2]
    subs    r0, #16
    cmp     r0, #31
    bgt     .LBB0_2
You could PR something to LLVM that adds the detection for that and they might very well accept it. But nobody has yet because this just doesn’t show up enough in real programs for them to care.
Like, sure, it’d be neat if it could do this. It’d certainly be aesthetically pleasing. But would it actually help real programs much?
The fact that C lacks a nice byte-based-indexing syntax would make code generation slightly tricky, but straightforward code generation along with a simple optimization stage that can notice that (1) the constant 0x12345678 gets loaded into a register which never needs to be modified during loop execution, so the load might as well be hoisted, and (2) the address expression (I just realized I oopsed the code...)
void test2(int *arr, int n)
{
    n*=16;
    while((n-=16) >= 0)
    {
        *(int*)((char*)arr+n) += 0x12345678;
    }
}
(2) the address that's being dereferenced is the sum of the values in two other registers, and may thus use the [Rn,Rm] addressing mode without having to spend an instruction performing the addition. Apply those two things to an otherwise mindless translation of the code and one would achieve performance better than what clang manages.
20:16
I repeat my previous two messages!
The world today relies upon mountains of code which relies upon implementations behaving in a manner consistent with Ritchie's Language in more cases than mandated by the Standard, in order to accomplish tasks not contemplated by the Standard. Unfortunately, there is no formally recognized distinction between that language and Fortran-wannabe dialects of C.
@AlexisKing You earlier stated "Clang does a pretty good job at not being stupid, at the very least." The only obstacle to a 1990s compiler outperforming clang when generating Cortex-M0 code for the function above is that the Cortex-M0 hadn't been invented yet.
“Not being stupid” does not mean “generating the theoretically optimal code in all contexts”.
I would think it should mean "Generating code that's not significantly worse than what a relatively mindless translator would produce", however.
I actually don’t know that I agree that a mindless translator would generate better code than what Clang produces. 1990s compilers were extremely bad at this stuff.
1990s compilers would generate efficient machine code when fed source code that fit well with a machine's native operations.
In any case, the world would fall apart if C weren't usable as a high-level assembler, since no other language provides the semantics that C was designed to provide, and compilers like clang and gcc usually manage to provide.
I need to be off for awhile...
 
1 hour later…
21:34
@AlexisKing Unfortunately, there also doesn't seem to be any will to make the Standard match clang/gcc behavior nor vice versa. Given e.g. extern int x[1],y[1];, all standards through C18 make clear that implementations may at their leisure place x and y in a manner that would make (x+1)==y true throughout a program's execution, or in a manner that would make it false throughout a program's execution, but those would be the only possibilities.
Clang and gcc treat such a comparison as invoking "Anything can happen" UB except at -O0 and -Og. Bug reports were filed for both compilers, since the Standard expressly contemplates such comparisons and defines their behavior as described, but neither compiler even treats the construct in a manner consistent with having each execution of the comparison independently yield a value chosen from the set {0,1} in side-effect-free fashion.
I'm really not sure how one can distinguish between programs that clang and gcc are designed to process meaningfully, and those which they process meaningfully only by happenstance.
@supercat Could you link me to those bug reports?
@AlexisKing Don't have a link handy, but check the output for
extern int x[1],y[1];
int test(int *p)
{
    y[0] = 1;
    if (p==x+1)
        *p = 2;
    return y[0];
}
I’d just be curious to see how they responded to it. The standard is often ambiguous, and maybe they had a reason.
21:49
The response was, if I recall, that compilers had no way of processing such constructs correctly without disabling a lot of optimizations.
That seems unlikely to be true for this particular example.
BTW, the Standard says: Two pointers compare equal if and only if both are null pointers, both are pointers to the
same object (including a pointer to an object and a subobject at its beginning) or function,
both are pointers to one past the last element of the same array object, or one is a pointer
to one past the end of one array object and the other is a pointer to the start of a different
array object that happens to immediately follow the first array object in the address
space.
To process code in an efficient and correct manner, a compiler would need to be able to recognize that addresses may be substitutable for purposes of effective-address capculation, without being fully substitutable in all other ways as well.
I think fundamentally people who wanted to view C as a Fortran replacement developed an abstraction model based around that notion, and view aspects of the C Standard that are inconsistent with that model as defects in the Standard rather than their model; the proper way to resolve such issues would be to recognize a dialect which is consistent with the compilers' abstraction model, but also recognize the existence of a dialect that supports constructs that abstraction model cannot.
@supercat It seems to me that the behavior you are arguing is non-conforming is not the equality test (as the compilers do generate the code for it), but the read of y[0], yes?
22:05
BTW, a dialect based upon a "high-level assembler" abstraction model could accommodate most forms of useful optimizations if it the Standard's "abstract machine" state were treated as a cached form of the the ABI's machine state, requiring the use of explicit synchronization directives in places where "cache coherency" would otherwise be a problem. If such a model had been adopted in 1990, then...
...code including such directives could have been recognized as preferable to equivalent code without, and there would be little need for programs to be able to use such behavior without the directives. As it is, though, there's no defined means of accomplishing many things that programs need to be able to do.
@AlexisKing The presence of the equality comparison effecitvely corrupts the read.
@supercat Look, man, I want to be charitable to you, but when you start rambling about how the standards committee has gone astray and the compiler developers are all in a conspiracy opposed to all reason then it’s hard to take you seriously. People have made mistakes, no question, but the solutions are not as simple as you make them out to be, nor is the crisis so severe (which should be obvious seeing as others clearly are not as convinced that the status quo is so untenable).
@AlexisKing To put things another way, if an equality test between a "just past" pointer and a pointer to the following object were UB, then the clang/gcc behavior would be correct. I can think of no other abstraction model that would fit the design behavior of clang and gcc.
In any case, you are referencing the classic Defect Report 260 issue here. Personally, I agree with you that compilers’ behavior on this particular case is really not justifiable under the text of the standard. But pointer provenance seems to have been accepted as legitimate by the standards committee—that is, they have explicitly opted to decline specifying it away.
If the committee wanted to make this behavior unambiguously nonconforming, they could. It would be easy. But new revisions of the standard have been published, and they have repeatedly chosen not to. So compiler developers are functionally operating on the DR260 “case law” here.
Do I think this is an abdication of responsibility on behalf of the C standards committee? Yes! Absolutely. The C “standard” is pitiful. But nobody has stepped up to fix it.
22:30
A lot of software, including most software that runs on embedded platforms, is written in a form of high-level assembly language invented by Dennis Ritchie, which many compilers can be configured to process, at least with optimizations disabled, which augments the Standard with "Any parts of the Standard which would characterize an action as invoking UB are subservient to parts of the Standard, K&R2, and the documentation for the implementation and execution environment...
...which would otherwise specify the behavior". Many implementations, even with optimizations disabled, may in some corner cases slightly deviate from the behaivors that might be implied by that rule, but a prerequisite for allowing programs to work by specification is recognizing the existence of a dialect which specifies many actions' behavior as "Instruct the execution environment to do X, with whatever consequences result", without regard...
...to whether or how the execution environment specifies the consequences of the behavior. If a Commodore 64 is used to run code generated by a low-level C implementation targeting the 6502 and given (char volatile*)0xD020 = 7;, the screen border should turn yellow, regardless of whether the author of the C implementation knows anything about Commodore 64, screens, borders, or the color yellow, because the Commodore 64 hardware has a 4-bit latch which is triggered by writes to 0xD020, and...
...the output from that latch will be fed to a color-generation circuit when raster scanning is within neither the main displayable area nor a blanking interval.
Writing a langauge spec that would satisfy the needs of the extremely vast majority of code written for freestanding implementations (nearly all such code that doesn't use nonstandard toolset-specific syntax) would not be difficult. Recognize the notion of a platform ABI that defines a few operations, and then specify that a translator's job is to convert each function into some kind of build artifact such that an execution environment that satisfies all of a compiler's documented requirements
...would have no choice but to behave in a manner whose observable behavior as defined by the platform ABI would be consistent with performing the indicated sequence of steps. Note that in most cases, platform ABIs would give compilers a lot of flexibility with regard to how they process things, thus allowing efficient C code to be translated into efficient machine code.
A fundamental difference between the FORTRAN abstraction model and Dennis Ritchie's abstraction model is that in the FORTRAN abstraction model, a compiler is entitled to assume that programmers won't exploit any knowledge about programmers' inner workings beyond what the Standard would provide, while C was designed around the assumption that programmers would often know things about the execution environment that a compiler likely wouldn't know and in many cases couldn't know.
@AlexisKing The issues raised by such defect reports would evaporate if the Commitee would recognize that if for 90% of tasksthe most efficient way of accomplishing what needs to be done wouldn't involve doing X, allowing compilers to assume that programs won't do X might improve the performance of 90% of tasks, but woudl be at best counterproductive for the remaining 10%.
For the extremely vast majority of programs and potential optimizations the Standard tries to use tricky rules to allow, it would be easy to identify at least one of the following as being true: (1) the optimization could be applied broadly without adversely affecting the program, or (2) performance objectives could be satisfied without trying to apply that particular optimization at all. Specifying a means by which programmers can say which situation applies would make it easier...
...for compilers to reap maximum benefits from programs that can benefit from the optimizations, while at the same time making it easier for programmers to write code that works by specification, rather than as a consequence of "missed optimizations".
23:14
@supercat I kind of disagree with using that as a reason to not optimize all the code. For example, in a game, of course the render loop is more performance-sensitive than the initialization code, but still, long loading screens can be annoying.
@CPlus Even if one ranks portions of the code based on the maximum fraction of any one contiguous second of execution time that would be spent therein, for many executions of many programs the median value would be zero, and for many more programs the median value would never get very big. If every call to foo would result in the system spending 1 microseconds processing code within foo, and 100 microseconds processing code within bar, speeding up the code within foo by a factor...
...of a billion would yield less than a 1% overall performance improvement. Modifying a sscanf implementation so it doesn't precompute the length of the string being scanned, by contrast, might slash load times drastically.
23:43
Makes sense

  last day (2 days later) »