Subtraction is not comparison
tedunangst.com
tedunangst.com
int
intcmp(struct node *e1, struct node *e2)
{
- return (e1->i - e2->i);
+ return (e1->i < e2->i ? -1 : e1->i > e2->i);
}
The ternary operator is necessary, because in C a relational operator is evaluated to 0 (false) or 1 (true). So, the author added a special case for returning a negative value (indicating the first argument should be sorted before the second). return (e1->i > e2->i) - (e1->i < e2->i);Therefore i would argue this is just as risky as the original solution, and that it's better to use the ternary operator
The same is true of the equality operators (= and !=), the logical negation operator (!), and the logical boolean operators (&& and ||).
There are other C idioms based on this, like !!expr to squash a zero-or-non-zero expression down to zero-or-one.
"Programs must be written for people to read, and only incidentally for machines to execute."
integer cmp x, y often reduces to microcode that effectively runs:
- 3A: sub x, y, dummy_result (att syntax) <-- most likely^
- 2A: sub copy_of_x, y (intel syntax)
- 1A: push x, push y (or y, x), sub, update flags, pop
^ Depends heavily on the microarchitecture, because there's likely all sorts of extra features and ops that can be set in the same microinstruction cycle to implement the macroinstructions.In signed 2's-complement (mostly everything), zero flag just checks that all result bits to be zero and the less than flag is just the result's MSB (negative result if 1).
Signed and unsigned have their under/overflow issues. Being able to detect that and handle it in code, rather than crashing or silently producing undefined results, can be important, such as safely re-callocing memory (multiply overflow). (There are other issues like pointer/slice index over/underflow too, and these are related problems.)
The issues are usually around whether branch-free code is absolutely necessary or whether predicting branches wrong would incur disastrous pipeline stall/s (greatly depends on the specific use-case, especially inner loops... don't prematurely overoptimize).
I almost feel there needs to be something in-between IR languages like LLVM IR and general-purpose C, that's not assembly but still general-purpose, functional/imperative, static analysis and able to expose math and branching state/options/differences cleanly across a variety platforms without a fugly/verbose syntax.
To make up a pseudoreligion (if it's doesn't look like your preferred Turing-complete language, sorry) at random:
var x, y int32
# ... assign x and y somehow here
begin
x = x +! y # or x +=! y
rescue IntegerOverflowError
# ... int32.max + int32.max => (0xFFFFFFFF + 0xFFFFFFFF) & 0xFFFFFFFF => 0xFFFFFFFE (-2, int32)
rescue IntegerUnderflowError
# ... int32.min + int32.min => (0x7FFFFFFF + 0x7FFFFFFF) & 0xFFFFFFFF => 0xFFFFFFFE (-2, int32)
end
var s, t uint64
# ... assign s and t somehow here
begin
s *=! t
rescue IntegerOverflowError
# ... uint64.max * uint64.max => ... => 1 (1, uint64)
end above or equal: !C
above: !C and !Z
below or equal: C or Z
below: C
And the signed comparisons are: above or equal: N == V
above: N == V and !Z
below or equal: N != V or Z
below: N != V
And, just for completeness, for both signed and unsigned: equal: Z
unequal: !ZWould it be nice to be able to access the carry in and carry out as "variables", as well as the full imul/idiv if i.e. 32 args -> 64 bit full-precision results without dropping down into assembly. Perhaps it's unreasonable, but it seems there are few limited differences per generic instruction across processors that assuming they are all each "special unique snowflakes" is obviously untrue FUD.
Understanding architecture / assembly comes in handy to replace high-level branching code with branch-free code to avoid pipeline stalls when branch predictions (eg the branch prediction infrastructure) is guessing incorrectly. Also, being able to go down the stack is really helpful because there are most definitely bugs the way down.
I remember coming across a set of lecture slides for a CS course on architecture that used its own toy architecture with neither flags nor separate comparison instructions - saying that comparison is the same as subtraction gives people the entirely wrong idea, and leads to the sort of bugs mentioned in this article.
Disclaimer: not actually my original invention.
The sole problem seems to be that integers in C are not actually integers.
int cmp(int x, int y) {
return (int) (((long)x-(long)y)>>32);
}There are all kinds of code patterns that are sensitive to overflow, and people do use those patterns because they are simple. A program that does not fail on overflow is something almost illegible, and for most applications the only difference it can make is displaying the correct error message before closing, because the problem domain has no procedure for that.
C programmers are implicitly expected to estimate the value ranges they'll be dealing with, and correctly size their variables.
Hint: his example is fine (and thus, Ted is wrong).
But, since Ted is OpenBSD.. here come the downvotes.
As the poster below states, the example in the last link of Ted's posting has overflow problems.
/*compare.c*/
#include <stdio.h>
int compare( int x, int y )
{
return x - y;
}
void testCompare( int x, int y )
{
if ( compare( x, y ) < 0 )
{
printf( "%d is less than %d\n", x, y );
}
else
{
printf( "%d is greater than or equal to %d\n", x, y );
}
}
int main()
{
testCompare( 5, 10 );
testCompare( 10, 5 );
testCompare( 1987654321, -1987654321 );
return 0;
}
$ clang -o compare compare.c$ ./compare
5 is less than 10
10 is greater than or equal to 5
1987654321 is less than -1987654321
Another reason (a lame one) to return -1, 0, or 1 is that often the caller expects one of those return values for some reason (because the caller is dumb and your comparison function is replacing another one that behaved that way).
x = 1987654321 and y = -1987654321? Then the difference between them is -319658654 (negative) which proves that x is less than y. That’s less than correct.
Which is completely 100% wrong. Surely the difference would be x - y i.e (1987654321 - (-1987654321)) = (1987654321 + 1987654321) = 3975308642.Which is perfectly ok, because that's positive and so proves x is greater than y. So this comparison works just fine for negative integers...
Overflow makes intuitive arithmetic do unusual things.
Similarly, using a+b/2 for binary search midpoints is wrong. http://googleresearch.blogspot.com/2006/06/extra-extra-read-...
The previous commenter may have entered that into say, a Python console where it wouldn't exhibit that behavior.
So what then is the CORRECT way of doing this comparison in C, avoiding potential overflow pitfalls?
return (x > y) - (x < y);From GCC documentation:
>>-findirect-inlining Inline also indirect calls that are discovered to be known at compile time thanks to previous inlining. This option has any effect only when inlining itself is turned on by the -finline-functions or -finline-small-functions options.
Enabled at level -O2.For many data types, like time, that will normally never happen, making subtraction the safest no-brainpower-needed approach. If in doubt, use the next larger integer type.
return (x > y) - (x < y);
Quick. What's that do?What will the next person who looks at the code think it does?
I think tptacek must be ready to have his bib changed, with all the drooling he must be doing.
int cmp(int x, int y) {
const unsigned a = x;
const unsigned b = y;
const unsigned s = sizeof x * 8 - 1;
return ((b^((b^(b-a))&(a^(b-a))))>>s)&~-((a^((a^(a-b))&(b^(a-b))))>>s)^-((a^((a^(a-b))&(b^(a-b))))>>s)&~-((b^((b^(b-a))&(a^(b-a))))>>s);
} return (x > y) - (x < y);
Is a common expression which is very efficient (why else would you write anything in C if you don't care about code being fast/efficient?) and what you proposed is neither.Duh.
I actually like and use C everyday, but it would be aberrant for me to say essentially what the article implies: "A math algorithm is wrong because it doesn't work in C"