Nearly All Binary Searches and Mergesorts Are Broken (2006)
research.googleblog.com
research.googleblog.com
Of course sorting 2^63 elements would require the different calculation.
y = a[0]*y[-1] + a[1]*y[-2] + b[0]*x[-1] + b[1]*x[-2]I'm not sure if you've misunderstood this (some of the other comments mentioning type certainly have) but size_t being large enough for the purpose of indexing an array is absolutely not the issue. The issue, illustrated by the glibc code pishpash shared, is that a size_t (or any other integer type) is not necessarily large enough to hold the sum of two other variables of the same type without overflowing.
#include <stdio.h>
#include <stdint.h>
int main() {
printf("size_t bytes: %u\n", sizeof(size_t));
size_t high = SIZE_MAX;
size_t low = high-1;
size_t mid_correct = low+(high-low)/2;
size_t mid_incorrect = (low+high)/2;
printf("low: %.ju\n", low);
printf("high: %.ju\n", high);
printf("low+high: %.ju\n", low+high);
printf("(low+high)/2 -- incorrect: %.ju\n", mid_incorrect);
printf("low+(high-low)/2 -- correct: %.ju\n", mid_correct);
}
On my 64-bit machine, I get: size_t bytes: 8
low: 18446744073709551614
high: 18446744073709551615
low+high: 18446744073709551613
(low+high)/2 -- incorrect: 9223372036854775806
low+(high-low)/2 -- correct: 18446744073709551614The unsigned types are generally awful; they have a big discontinuity right at zero, so you have to be careful with arithmetic even if it involves small values.
If I'm working with int and I know that all quantities fit into two decimal digits, then there isn't possibly any problem. Not so with unsigned int or size_t or what have you.
If a, b, c are small integers in a signed type then I know I can rewrite a < b + c as a - c < b, following straight algebraic rules of derivation. Not so if these are unsigned; moving c from the right side to the left may break the inequality because a may be smaller than c.
You might argue that you're never going to overflow a size_t on a 64-bit, maybe, but given that the correct code is right there above in the article, it seems easy enough to just do the right thing (add half the delta to the lower bound, which avoids overflow all together for unsigned integer inputs).
The input is an array of int, ints are 4 bytes, 32 bit systems can only address 2^32 bytes, so the array is no more than 2^30 elements long, so in the worst case, low+high equals 2^31-3, less than even a signed it.
It could overflow if we pass it a char* instead but if you have a >2GB array of sorted single bytes, you probably have a problem somewhere else...
Computer scientist: "The bug is in the language. Signed integer overflow behavior should have been defined in such a way as to guarantee correct functionality in cases such as this."
Me: "Use int64s for this sort of thing. It's still broken, but I'll be retired or dead before anyone notices."
Engineer: "The bug is in the documentation. The program is correct but should have been specified for use with element counts no greater than INT_MAX / 2."
Mathematician: "A solution exists."
Manager: "Ship it."
int mid = (low + high) >>> 1;
I suppose things like this is why the ">>>" (unsigned shift) operator exists - but it's a bit odd when the value it works on is considered signed by the language, and implemented as two-compliment signed in memory.What's interesting to me is that this allows the sum to overflow, and would fail with the ">>" operator as far as I can tell (signed shift) - just like the original code would fail with simply dividing by 2.
Guess it shows java's "system language" roots - in that one might expect there to be a way to be alerted to overflow when working with signed integers - but the solution here is to use a special operator to "fix" the problem.
Maybe it's just me, but it's a solution that would feel more at home in assembler, than I personally think it does in java.
It feels like subtle subversion of java's admittedly strange type system for numbers (mix of raw integer types and boxed numbers is never going to be pretty...).
https://fossies.org/dox/glibc-2.25/stdlib-bsearch_8h_source....
__idx = (__l + __u) / 2;
__idx, __l, and __u are all the same type and the addition of __l and __u could cause an overflow resulting in a nonsense value assigned to __idx.
That may fix things in the case of searching a sorted array, but binary search can be used more generally than that. I think that fix might not work for some of the more general applications of binary search.
For instance, suppose f(n) is an increasing function from the signed integers to the signed integers, with f(a) < 0 and f(b) > 0, and you want to find an n in (a,b), if such n exists, such that f(n) = 0. Binary search on [a, b] is a reasonable approach.
If a < 0 and b > 0, then that mid computation could overflow on the subtraction.
int mid = (low + high) / 2;
but you'd have to constrain low and high to be in the range [-2^30, 2^30 - 1] so as to not overflow a (assuming 32-bit) signed integer. (And you're probably using 64-bit floating point, or similar, which has other fiddly bits.)However, the article specifically talks about merge sort in arrays, so low (your "a") is always >= 0. The discovery was "oh, we went decades before someone had to sort an array with more than 2^31 elements". Frankly, it's akin to when we ran out of IPv4 addresses -- when it was originally built, there was some range over which the computation was defined to be correct, and we later discovered that we wanted to do computation outside that range.
(low / 2) + (high / 2)
but then I have to think about rounding error. Ugh. I guess you could write a bunch of nested `if`s to handle the parity errors, assuming you know how your language rounds when dividing negative numbers. (I wouldn't be surprised if C leaves that "implementation-defined".) If you're really searching an arbitrary range, maybe just use bigints. Then at least you can stop worrying about overflow altogether. mid = lo / 2 + hi / 2;
Well, that is, if your algorithm can tolerate a mid == 6, for hi == 7 and lo == 7. :) :) :)Cough, cough; seriously though: here is a variant based on the above idea which takes care of the remainders, avoiding that problem:
;; TXR Lisp
(defun mid (lo hi)
(tree-bind ((lq lr) . (hq hr)) (cons (trunc-rem lo 2) (trunc-rem hi 2))
(+ lq hq (trunc (+ lr hr) 2))))
trunc-rem has toward-zero truncation, with a remainder that is harmonized to that.Based on my testing in the REPL, this is behaving sensibly.
Adding the quotients, and then adding to them the sum of the remainders, truncated by two, seems to be doing the trick.
(mid 7 7) is 7, (mid -7 -7) is -7 and various other cases are all sensible. (mid k (+ 2 k)) is yielding (+ 1 k), for both values being negative, either being zero, and zero-crossing cases. (mid k k) seems to be k for all k.
I think as of ISO C99, the / and % operators truncate toward zero. Unless I'm gravely mistaken, this is then nicely expressible in C as:
lo / 2 + hi / 2 + ((lo % 2) + (hi % 2)) / 2;
It's mathematically well founded: we have added the truncations, and then continue working with the remainders. This is because the following expression in fact expresses the exact result. trunc(lo,2) + trunc(hi,2) + (rem(lo, 2) + rem(hi, 2))/2
where / is exact rational division! Dividing the remainders by two just continues the inexact division, completing it to exactness. What we're doing differently in the machine calculation is using truncating division on the sum of the remainders, which we need because mid is to be an integer result. return -(low + 1); // key not found.Edit: I'm quite surprised I'm being modded down given the magnitude of what I'm implying. I suspect someone doesn't understand what I'm saying.
I certainly didn't realize how generalized discrimination-based sorts were. Many people I've talked to outside of the Haskell community don't know at all. I'm still working through the papers, the base of that chain is like 86 pages long!
This search fallacy has been corrected. I'm mentioning one that hasn't.
http://www.diku.dk/hjemmesider/ansatte/henglein/papers/henglein2011a.pdf
http://www.diku.dk/hjemmesider/ansatte/henglein/papers/henglein2011c.pdf
https://www.youtube.com/watch?v=sz9ZlZIRDAg
https://hackage.haskell.org/package/discrimination
I didn't know about this until I read your reply and googled; I thought you were just obliquely referring to radix sort or something like that.I can't believe I got down voted for this thread. What does it take?
I guess I should just post it.
It's interesting how much things have changed in the last decade. I wonder what next decade's "Rails" will be? Could it be possible to predict?
Edit: Did not mean to "mock" the OP, just merely trying to showing that you can apply that thought to almost anything in the programming world by replacing the technology/language names.
http://thecodist.com/article/the_programming_steamroller_wai... is a good essay on this.
EDIT: previous discussion: https://news.ycombinator.com/item?id=7204515
EDIT: I swear I did not read the top comment in the (newly) linked HN discussion of that article before I wrote my response. I agree completely.
But then, I wonder if it just seems that way to me because I'm a product of "now", which makes me more proficient in how we do things now, which makes it seem easier. It's possible neither is easier, that they are just different, and for everyone, the other seems harder than the one they already know.
I'm not trying to argue that this is the case: I could see it being either way and I sincerely wonder which way it is.
I think that's your issue.
Just my guess.
http://www.newsweek.com/2017/04/21/quantum-computing-ibm-580...
Understanding floating point arithmetic is even more important than over/underflow of integers in dynamic languages such as Python (see e.g. https://www.python.org/dev/peps/pep-0237/).
A smart compiler should have caught (a + b) / 2 and fixed it to be correct, there's no way the overflow situation is what the programmer wanted.
On one hand, it is true that the tooling is possible. On the other hand, it remains an open problem.
https://pdos.csail.mit.edu/6.828/2016/xv6/book-rev9.pdf
https://pdos.csail.mit.edu/6.828/2012/xv6/xv6-rev7.pdf
The labs are very informative: https://pdos.csail.mit.edu/6.828/2016/labs/lab3/
I also worked through the 6.824 Distributed Systems course, which is a lot of fun:
https://pdos.csail.mit.edu/6.824/
Lab 4 is fiendishly difficult, but you end up learning a lot.
Beyond all that, there's no substitute for just diving in and breaking stuff. Just go with whatever area you find the most fun. Maybe that's compiling and modifying the Linux kernel, or maybe it's tweaking a software rasterizer like https://github.com/blitzcode/rust-exp.
I got into systems programming mainly as a consequence of being a game developer -- when you write your own engines, you're forced to consider a hundred low-level details like memory layout, pipelining, threading, simulation, adding a scripting interface for your designers, cursing AMD when you run into GPU driver bugs, etc. There are a lifetime worth of interesting problems to solve in that area.
Not that it isn't a potential problem, but it's a narrow issue.