http://qs321.pair.com?node_id=694221


in reply to find closest element of array without going over

"...which just seems wasteful,"
Not as inefficient as you might think. Perl, and the modern processor, is highly optimized for this case. Does your application really require optimal performance here? I think you'd be really hard pressed to code something that was significantly faster. Consider that the whole thing, minus the array, could easily fit into one of the processor's ALU pipelines. The only thing that is going to slow this down is if the array exceeds the L2 cache. That would be how many millions of integers on your machine?

For those whose advocate a binary search, I think that if you were to code it you'd find very little improvement. ( Been there, done that, have the shirt. ) Considering the difficulty in coding a binary search correctly, the likelihood in having bugs therein, and the programmer effort required I would have a hard time recommending such.

s/millions/hundred thousand/


s//----->\t/;$~="JAPH";s//\r<$~~/;{s|~$~-|-~$~|||s |-$~~|$~~-|||s,<$~~,<~$~,,s,~$~>,$~~>,, $|=1,select$,,$,,$,,1e-1;print;redo}
  • Comment on Re: find closest element of array without going over

Replies are listed 'Best First'.
Re^2: find closest element of array without going over
by moritz (Cardinal) on Jun 26, 2008 at 18:31 UTC
    I took that as a challenge ;-). I didn't test my binary search with multiple identical values, thus I don't count it as a full implementation. But see the speed differences as a function of the array size:
    100 items Rate brutal binary brutal 186657/s -- -16% binary 223418/s 20% -- 1000 items Rate brutal binary brutal 20574/s -- -87% binary 156038/s 658% -- 10000 items Rate brutal binary brutal 2167/s -- -98% binary 129102/s 5859% -- 100000 items Rate brutal binary brutal 169/s -- -100% binary 91542/s 54205% --
    And this is the code I used:

    The index of $limit / 3 to search for actually favors the "brutal" search, since it means the search terminates earlier than in random conditions.

      It's all a matter of perspective. In the OP's query, he had 18 items in his list. Taking your code, changing the $search_for to be $limit/2 to better approximate actual random values, and using an input of 17 (since you seem to add one), I get:

      $ perl ./x.pl 17 Done generating list 9 9 Rate brutal binary brutal 449367/s -- -17% binary 544651/s 21% --
      So the difference is ~20%. But what are we really talking about here? On my system, that difference is really dropping from a binary search's 2.23 µs to 1.84 µs. A savings of 0.39 µs. Seriously, we're quibbling here? (Binary search should do REALLY well here, too, picking nearly the first item, I think)

      In your cases, I see at 100 (101, really, but again, let's not quibble about small details) dropping from 5.4 µs to 4.5 µs, a savings of 0.88 µseconds. At 1000, it's 48.6 µs vs 6.41 µs, or 42.2 µs savings. Much bigger numbers. But, seriously, is it a concern?

      At 10,000, it's 461 µs vs 7.75 µs, or a savings of 454 µs. We're still under a thousandth of a second here, folks. Even at 100,000 items in the (pre-)sorted list, we're comparing 5.92 ms vs 10.9 µs. Sure, that's a savings of nearly the whole thing, but that's still only saving 5.91 ms. Really, do we care?

      Now, granted, you ran these tests skewed (looking for something 33% in instead of half-way, otherwise binary search wouldn't even have to do anything), but the reality is (and I think starbolin's point is) that doing this search really doesn't take much time. Worrying about this before you've profiled anything is simply premature optimisation, and you'd get bigger bang for your buck (that being programmer's time) if you'd spend it on something else. The reward for all the time spent coding, testing, and fixing a binary search just does not pay for itself, at least not if you're merely running it once, for a small number (i.e., less than a million, it seems).

      Changing $search_for to my $search_for = $a[int($limit * (1/2 - 1/9))];, and rerunning at 1_000_000 entries shows a savings of about 66.7ms on my machine. It's simply not worth it. Running one more time for 10 million, and the savings is 775 ms (from 775ms minus 14.1 µs). Now it's starting to be worth it. If it's being run interactively. If it's a long-running program that users expect to walk away from and come back, then even that's no big deal.

      Of course, this may all be moot if it's code for perlmonks or slashdot or some other active site, but you still should profile before worrying about such small improvements. Chances are, your bottlenecks still are elsewhere.

      Update: I should point out that I realise that bunchmarking a static index for an O(n) vs O(lg n) comparison is inherently unfair, and an equal distribution across the entire problem space would need to be concocted for truely accurate numbers. However, even if that resulted in numbers two or three times what I provide above (which would not be the case since the upper limit is on the brute-force method, and that would not increase by much), that would not change the conclusion. 18ms is not much more time than 6ms (and the true number would probably be still under 8ms at 100,000 on this machine).

        If sadmanface hadn't worried about speed, he wouldn't have asked that question, I assume.

        Since I couldn't give an algorithm that is faster in the case of small numbers, I gave one that is very much faster for big numbers. He can chose whatever is best for his application.

        As to the difference of absolute times, I can just say that it scales with number of times the algorithm is run. Again with can just try to credit sadfaceman some intelligence and assume he wouldn't ask his question about this particular item of code if it wasn't of importance. After all, it cost him some time to ask as well.

        Yes, it's a good idea to benchmark, but it still don't think that's a reason to recommend an algorithm that scales bad in the general case, unless you state its limits. And I don't think it's bad to recommend an algorithm that scales well in the general case, even it might be a bit slower for small data structures.

      Except I see at least five errors in your code.

    • First, your code finds the next element 'greater' than the input not 'ge'. Also it returns the index not the value ( See 'Fourth' below )
    • Second,
      my $tmp = ($right+$left) / 2; if ($a[$tmp] > $search_for){ ...
      Very interesting indexing into an array with a float. I know perl will DWIM but although perl will round toward 0, I don't think it's guaranteed.
    • Third, $right+$left will overflow silently for numbers greater than 1e31 on many systems. This is a rare occurrence in these days of 32 bit int but it's there nevertheless.
    • Fourth, your code returns an empty list element if $search_for is at the end of the array. While this my be a usefull error return, should the programmer attempt to use the index to retrieve a value from the array, the program would break.
    • Fifth, in your example you define your array to be one larger than what you search over. While this is more an implementation problem than an algorithm problem the example is nonetheless in error.
    • I'm being picky here because the code illustrates that a quick-search is difficult to write correctly. Your code makes my point perfectly, thank you.

      Update: Three is a Red Herring. When first I looked at your code I said "Ah, there is the Binary-Search-Overflow bug!" But of course this was a bigger problem when long was the memory space and int was shorter than long. But Perl is written so that an int can access the total memory space and so three is less of a bug that any other operation on large integers would be.


      s//----->\t/;$~="JAPH";s//\r<$~~/;{s|~$~-|-~$~|||s |-$~~|$~~-|||s,<$~~,<~$~,,s,~$~>,$~~>,, $|=1,select$,,$,,$,,1e-1;print;redo}