5 ms·
Why didn't they just... define it, back when they wrote it?
by voidUpdate 2y ago
Why didn't they just... define it, back when they wrote it?
- deleted 2y ago[deleted]
- deleted 2y ago[deleted]
- frabert 2y agoEvery time they leave something undefined, they do so to leave implementations free to use the underlying platform's default behavior, and to allow compilers to use it as an optimization point
- jcelerier 2y agoHere it's more that it allows to assume that this is never the case, thus no need to have an additional check in it I assume ?
- lucozade 2y ago> time they leave something undefined, they do so to leave implementations free to use the underlying platform's default behavior That's implementation defined (more or less) ie teh compiler can do whatever makes mst sense for its implementation. Undefined means (more or less) that the compiler can assume the behaviour never happens so can apply transforms without taking it into account. > to allow compilers to use it as an optimization point That's the main advantage of undefined behaviour ie if you can ignore the usage, you may be able to apply optimisations that you couldn't if you had to take it into account. In the article, for example, GCC eliminated what it considered dead code for a NULL check of a variable that couldn't be NULL according to the C spec. That's also probably the most frustrating thing about optimisations based on undefined behaviour ie checks that prevent undefined behaviour are removed because the compiler thinks that the check can't ever succeed because, if it did, there must have been undefined behaviour. But the way the developer was ensuring defined behaviour was with the check!
- frabert 2y agoAFAIK, something having undefined behavior in the spec does not prevent an implementation- (platform-)specific behavior being defined. As to your point about checks being erased, that generally happens when the checks happen too late (according to the compiler), or in a wrong way. For example, checking that `src` is not NULL _after_ memcpy(sec, dst, 0) is called. Or, checking for overflow by doing `if(x+y<0) ...` when x and y are nonnegative signed ints.
- nephanth 2y agoI mean, they might not have given thought to that particular corner case, they probably wrote something like > memcpy(void* ptr1, void* ptr2, int n) Copy n bytes from ptr1 to ptr2. UNDEFINED if ptr1 is NULL or ptr2 is NULL ‐------ It might also have come from a "explicit better than implicit" opinion, as in "it is better to have developers explicitly handle cases where the null pointer is involved
- jbverschoor 2y agoI think it's more a strategy. C was not created to be safe. It's pretty much a tiny wrapper around assembler. Every limitation requires extra cycles, compile time or runtime, both of which were scarce. Of course, someone needs to check in the layers of abstraction. The user, programmer, compiler, cpu, architecture.. They chose for the programmer, who like to call themselves "engineers" these days.
- wruza 2y agoNot sure what your last remark means wrt everything else.
- poincaredisk 2y agoI disagree with your premise. C was designed to be a high level (for its time) language, abstracted from actual hardware >It's pretty much a tiny wrapper around assembler Assebler has zero problem with adding "null + 4" or computing "null-null". C does, because it's not actually a tiny wrapper.
- jbverschoor 2y agoNot high-level.. Portable. Portable layer above assembler/arch. NULL doesn't exist in assembler, and in C, NULL is only a defined as a macro. It's not something built-in. C doesn't have any problems adding 4 to NULL nor subtracting NULL from NULL.
- jbverschoor 2y ago
- larschdk 2y agoWhen C was conceived, CPU architectures and platforms were more varied than what we see today. In order to remain portable and yet performant, some details were left as either implementation defined, or completely undefined (i.e. the responsibility of the programmer). Seems archaic today, but it was necessary when C compilers had to be two-pass and run in mere kilobytes of RAM. Even warnings for risky and undefined behavior is a relatively modern concept (last 10-20 years) compared to the age of C.
- actionfromafar 2y agoWhen C was conceived, it was made for a specific DEC CPU, for making an operating system. The idea of a C standard was in the future. If you wanted to know what (for instance) memcpy actually did, you looked at the source code, or even more likely, the assembler or machine code output. That was "the standard".
- anticensor 2y agoNo, K&R's book was the standard.
- actionfromafar 2y agoFirst came the language, then a few years later they described it in a book.
- da_chicken 2y agoI think it's reasonable to assume that GP clearly meant the C standard being conceived, as, obviously, K&R's C implementation of the language was ad hoc rather than exhibiting any prescribed specification.
- scoutt 2y ago> Seems archaic today ... run in mere kilobytes of RAM There is an entire industry that does pretty much that... today. They might run in flash instead of RAM, but still, a few kilobytes. Probably there are more embedded devices out there than PCs. PIC, AVR, MSP, ARM, custom archs. There might be one of those right now under your hand, in that thing you use to move the cursor.
- hyperman1 2y agomemcpy used to be a rep movsb on 8086 DOS compilers. I don't remember if rep movsb stops if cx=0 on entry, or decrements first and wraps around, copying 64K of data.
- connicpu 2y agoI know at least MSVC's memcpy on x86_64 still results in a rep movsb if the cpuid flag that says rep movsb is fast is set, which it should be on all x86 chips from about 2011/2012 and onward ;)
- dfox 2y agoThe specification does not explicitly say that, but the clear intention is that REP with CX=0 should be no-op (you get exactly that situation when REP gets interrupted during the last iteration, in that case CX is zero and IP points to the REP, not the following instruction).
- bonzini 2y agoRep movsb copies 64K if CX=0 (that's actually very useful), but memcpy could be implemented as two instructions: jcxz skip rep movsb skip:
- wat10000 2y agoThe original C standard was more descriptive than prescriptive. There was probably an implementation where it crashed or misbehaved.
- menaerus 2y agoCharitable interpretation may be: Back then when the contract of this function was standardized, presumably in C89 which is ~35 years ago, CPUs but also C compilers were not as powerful so wasting an extra couple of CPU cycles to check this condition was much more expensive than it is today. Because of that contract, and which can be seen in the example in the below comments, the compiler is also free to eliminate the dead code which also has the effect of shaving off some extra CPU cycles.
- lmm 2y agoBack when they wrote it they were trying to accommodate existing compilers, including those who did useful things to help people catch errors in their programs (e.g. making memcpy trap and send a signal if you called it with NULL). The current generation of compilers that use undefined behaviour as an excuse to do horrible things that screw over regular programmers but increase performance on microbenchmarks postdates the standard.
- FartyMcFarter 2y agoBecause the benefit was probably seen as very little, and the cost significant. When you're writing a compiler for an architecture where every byte counts you don't make it write extra code for little benefit. Programmers were routinely counting bytes (both in code size and data) when writing Assembly code back then, and I mean that literally. Some of that carried into higher-level languages, and rightly so.
- killerstorm 2y agoFrom what I understand: 1. Initially, they just wanted to give compiler makers more freedom: both in the sense "do whatever is simplest" and "do something platform-specific which dev wants". 2. Compiler devs found that they can use UB for optimization: e.g. if we assume that a branch with UB is unreachable we can generate more efficient code. 3. Sadly, compiler devs started to exploit every opportunity for optimization, e.g. removing code with a potential segfault. I.e. people who made a standard thought that compiler would remove no-op call to memcpy, but GCC removes the whole branch which makes the call as it considers the whole branch impossible. Standard makers thought that compiler devs would be more reasonable
- kllrnohj 2y ago> Standard makers thought that compiler devs would be more reasonable This is a bit of a terrible take? Compiler devs never did anything "unreasonable", they didn't sit down and go "mwahahaha we can exploit the heck out of UB to break everything!!!!" Rather, repeatedly applying a series of targeted optimizations, each one in isolation being "reasonable", results in an eventual "unreasonable" total transformation. But this is more an emergent property of modern compilers having hundreds of optimization passes. At the time the standards were created, the idea of compilers applying so many optimization passes was just not conceivable. Compilers struggled to just do basic compilation. The assumption was a near 1:1 mapping between code & assembly, and that just didn't age well at all.
- LegionMammal978 2y agoOne could argue that "optimizing based on signed overflow" was an unreasonable step to take, since any given platform will have some sane, consistent behavior when the underlying instructions cause an overflow. A developer using signed operations without poring over the standard might have easily expected incorrect values (or maybe a trap if the platform likes to use those), but not big changes in control flow. In my experience, signed overflow is generally the biggest cause of "they're putting UB in my reasonable C code!", followed by the rules against type punning, which are violated every day by ordinary usage of the POSIX socket functions.
- ynik 2y agoProbably because they did not think of this special case when writing the standard, or did not find it important enough to consider complicating the standard text for. In C89, there's just a general provision for all standard library functions: > Each of the following statements applies unless explicitly stated otherwise in the detailed descriptions that follow. If an argument to a function has an invalid value (such as a value outside the domain of the function, or a pointer outside the address space of the program, or a null pointer), the behavior is undefined. [...] And then there isn't anything on `memcpy` that would explicitly state otherwise. Later versions of the standard explicitly clarified that this requirement applies even to size 0, but at that point it was only a clarification of an existing requirement from the earlier standard. People like to read a lot more intention into the standard than is reasonable. Lots of it is just historical accident, really.