8

I have heard that there is no faster algorithm faster than linear search (for an unsorted array), but, when I run this algorithm (linear):

public static void search(int[] arr, int value){
    for(int i = 0; i < arr.length; i++){
        if(arr[i] == value) return;
    }
}

With a random array of length 1000000, the average time to find a value is 75ns, but with this algorithm:

public static void skipSearch(int[] arr, int value){
    for(int i = 0; i < arr.length; i+=2){
        if(arr[i] == value) return;
    }
    for(int i = 1; i < arr.length; i+=2){
        if(arr[i] == value) return;
    }
}

I get a shorter average, 68ns?

Edit: A lot of you are saying that I didn't do a proper benchmark and this was by fluke, but I ran these functions 1000000 times and got the average. And every time I ran the functions 1000000 times, I got 75-76ns for the first algorithm, and 67-69ns for the second algorithm.

I used java's System.nanoTime() to measure this.

Code:

int[] arr = new int[1000];
Random r = new Random();
for(int i = 0; i < arr.length; i++){
    arr[i] = r.nextInt();
}
int N = 1000000;
long startTime = System.nanoTime();
for(int i = 0; i < N; i++){
    search(arr, arr[(int) Math.floor(Math.random()*arr.length)]);
}
System.out.println("Average Time: "+(System.nanoTime()-startTime)/(float)N+"ns");
startTime = System.nanoTime();
for(int i = 0; i < N; i++){
    skipSearch(arr, arr[(int) Math.floor(Math.random()*arr.length)]);
}
System.out.println("Average Skip Search Time: "+(System.nanoTime()-startTime)/(float)N+"ns");
programmers5
  • 326
  • 2
  • 13
  • 2
    I don't believe it. Did you do a proper benchmark? – Paul Boddington Oct 29 '15 at 23:47
  • 24
    Another person misled by a meaningless benchmark. – duffymo Oct 29 '15 at 23:49
  • 11
    What makes you think your `skipSearch` isn't linear? – trooper Oct 29 '15 at 23:49
  • I've read your edit. Can you include the exact code you used? – Paul Boddington Oct 30 '15 at 00:00
  • There, I put the code up – programmers5 Oct 30 '15 at 00:06
  • 3
    Java gets faster the longer you run it. Try testing skipSearch first. Also try using index n%1000 instead of a random. – Matt Timmermans Oct 30 '15 at 00:21
  • 1
    What happens if you increase N to 10_000_000_000? Or to 100_000_000_000? Will you get the same benchmarks? :) – Honza Zidek Oct 30 '15 at 00:41
  • 1
    Funnily, the answer to this question was in entirely different direction. JIT optimizations. – displayName Oct 30 '15 at 03:06
  • Re: *1000000 times! That seems random enough* - "How do you know that 1000 is the correct number of iterations to improve the power of the experiment?" and "If all you do is run 1000 and then take an average, then how do you spot places where the system is really hurting?" - [Power-of-Ten Syndrome section of this essay](http://zedshaw.com/archive/programmers-need-to-learn-statistics-or-i-will-kill-them-all/). – TessellatingHeckler Oct 30 '15 at 03:17
  • You changed the length of the array in the code, but the text still says `With a random array of length 1000000`. Consider including "microbenchmark" in the title. – greybeard Oct 30 '15 at 04:56
  • Probably a stupid remark, but one thing the other answers don't mention is `i++` vs `i+=2`. If you do want to run benchmarks, run the first with `i+=1` or `++i` and see if that makes a difference. (Just make sure optimisations are off.) – Mr Lister Oct 30 '15 at 10:44
  • 4
    Both algorithms are linear thus making the question invalid. – this Oct 30 '15 at 12:39
  • @MattTimmermans - Yeah, I always throw out the first couple results when benchmarking anything in Java. My guess is that on a fair comparison (not randomized, and not under the influence of Java startup-hangover), the regular search is going to be marginally faster than skipSearch, due to memory locality. Skip search will have to swap memory twice as often to hit all the data. – Darrel Hoffman Oct 30 '15 at 13:34
  • 1
    See also http://stackoverflow.com/questions/504103/how-do-i-write-a-correct-micro-benchmark-in-java – meriton Oct 31 '15 at 22:14
  • 2
    Receiving a time as short as 60ns for finding a very likely unique entry on an array of size 1000 is not plausible: On average, finding a random element of the array would require 500 comparisions. Assuming a 4Ghz processor, that's about 2 comparisions per clock cycle! – meriton Oct 31 '15 at 22:18
  • 1
    @meriton: exactly! The unbelievably small time is the hint which directs to thinking about being mislead by JIT optimization. – Honza Zidek Nov 02 '15 at 12:53
  • @programmers5: You might consider changing the title of your question to something like "Why does my benchmark give suspiciously too good result?", or at least add this sentence to the text for search engines. – Honza Zidek Nov 02 '15 at 12:59

8 Answers8

31

It's quite possible that, as your search() methods do not return anything, and there isn't any action inside the loops, the JIT compiler in your JVM optimizes the code - in other words, modifies the byte-code before loading it to JVM so that both your search() methods most probably do not do (almost) anything. Which is most significant, it probably also completely removes the loops. JIT optimization is pretty smart, it can identify a lot of situations when it is not needed to load any code into JVM (however the code is in the byte-code .class file).

Then you measure just random numbers - not the real time complexity of your methods.

Read e.g. how to make sure no jvm and compiler optimization occurs, apply it and run your benchmark again.

Also change your search() methods so they return the index - thus making the life for the optimizer harder. However, sometimes it's surprisingly difficult to create a code which is impossible to be optimized :) Turning off the optimization (as in the link above) is more reliable.


Generally it doesn't make sense to benchmark unoptimized code. However, in this case the OP wants to measure a theoretical algorithm. He wants to measure the real number of passes. He has to ensure that the loops are actually performed. That's why he should turn the optimization off.

The OP thought that what he had measured was the speed of the algorithm, while in fact the algorithm had not even had a chance to run at all. Turning the JIT optimization off in this particular case fixes the benchmark.

Community
  • 1
  • 1
Honza Zidek
  • 9,204
  • 4
  • 72
  • 118
  • What is the point of benchmarking unoptimized code? It just teaches you to manually do the optimizations that usually happen automatically. – nwp Oct 30 '15 at 13:02
  • +1 It does seem that his machine is either optimising a lot, or is many times faster than my netbook. Running the tests more than once gives different results - mine are about 1140ns/1150ns first time then stabilizes to about 1090/1220 once it's warmed up, so I'd call the OP's result a fluke. – Pete Kirkham Oct 30 '15 at 14:03
  • 2
    Ya, no; you don't get to turn off optimizations and make *any* useful performance claims about the result, unless you plan on shipping code with optimizations disabled. Your point that the code is optimized to do nothing is good; your advice to "fix this" by turning off optimizations is bad. If you cannot generate a task that the compiler cannot optimize away, you shouldn't benchmark it and draw conclusions from it anyhow. – Yakk - Adam Nevraumont Oct 30 '15 at 14:06
  • 5
    The proposition isn't to turn off optimization in order to ship code, the proposition is to run benchmarks with the optimizations disabled so that the OP might obtain proof that linear search is indeed faster in a real-world scenario. – Cronax Oct 30 '15 at 15:06
  • 2
    Turning off optimization is not a valid method for measurement even of theoretical cases like this. Instead you need to include constraints so that the output is actually used (like printing the result as part of the timing measurement) and use non-constant inputs so that the whole thing can't be constant-folded. You *want* to know how fast the optimized code runs, not how fast the "un-optimized" (a better word would be "pessimized", since no well-engineered compiler actually produces "un-optimized" code in a natural manner) code runs. – R.. GitHub STOP HELPING ICE Oct 30 '15 at 15:50
  • @Cronax no, that shows almost nothing about a "real-world scenario", because in real-world scenarios, you have *optimizations on*. – Yakk - Adam Nevraumont Oct 30 '15 at 16:37
  • 3
    You're missing the point, the hypothesis is that peculiarities in the optimization are causing the custom search to outperform linear search. Hence why the proposition is to turn said optimization off so that the difference might be seen. As @R.. states, forcing the actual usage of the code is a better solution, but Honza Zidek wasn't suggesting a *fix* he was suggesting a way to *disprove* the OP's theory. – Cronax Oct 30 '15 at 16:42
18

This is why we are not concerned about literally timing how long things take to execute and more how things grow in scale as the complexity of the inputs increases. Have a look at asymptotic runtime analysis:

https://en.wikipedia.org/wiki/Analysis_of_algorithms

Colin Schoen
  • 2,526
  • 1
  • 17
  • 26
7

what is statistics of value ? Most likely it's even values in your case. It's quite clear that for both cases complexity of algorith O(n) and O(n/2) + O(n/2) that is pretty much same - linear time

vvg
  • 6,325
  • 19
  • 36
5

It's just by chance that it's "faster". What you are probably noticing is that your values appear more often on an even index, than on an odd index.

budi
  • 6,351
  • 10
  • 55
  • 80
3

In theory, the time complexity of both algorithms are the same O(n). One speculation why skipSearch was faster when you ran it is that the element you were searching for happened to be located at an even index, therefore it will be found by the first loop, and in the worst case it would do half the number of iterations of linearSearch. In benchmarks like these you not only need to consider the size of the data, but also what the data looks like. Try searching for an element that doesn't exist, an element that exists at an even index, an element that exists at an odd index.

Also, even if that skipSearch performs better using proper benchmarks, it still only shaves off a few nanoseconds, so there's no significant increase, and it's not worth using it in practice.

turingcomplete
  • 2,128
  • 3
  • 16
  • 25
  • Yes, but I tried picking a random index 1000000 times! That seems random enough to be either odd or even – programmers5 Oct 30 '15 at 00:15
  • @programmers5 you're also not searching for the same values using both methods. Each time you generate separate random values to search for in each method. This is also another reason why the results look fishy. – turingcomplete Oct 30 '15 at 00:19
3

One of the problems mentioned by someone was that you're using different indices for each algorithm. So, to fix this I reworked a bit of your code. Here's the code I have:

int[] arr = new int[1000];
Random r = new Random();
for(int i = 0; i < arr.length; i++){
    arr[i] = r.nextInt();
}
int N = 1000000;
List<Integer> indices = new ArrayList<Integer>();
for(int i = 0; i < N; i++){
    //indices.add((int) Math.floor(Math.random()*arr.length/2)*2); //even only
    indices.add((int) Math.floor(Math.random()*arr.length/2)*2+1); //odd only
    //indices.add((int) Math.floor(Math.random()*arr.length)); //normal
}

long startTime = System.nanoTime();
for(Integer i : indices)
{
    search(arr, arr[i]);
}
System.out.println("Average Time: "+(System.nanoTime()-startTime)/(float)N+"ns");

startTime = System.nanoTime();
for(Integer i : indices)
{
    skipSearch(arr, arr[i]);
}
System.out.println("Average Skip Search Time: "+(System.nanoTime()-startTime)/(float)N+"ns");
    

So you'll notice I made an ArrayList<Integer> to hold indices, and I provide three different ways of populating that array list - one with even numbers only, one with odd numbers only, and your original random method.

Running with even numbers only produces this output:

Average Time: 175.609ns

Average Skip Search Time: 100.64691ns

Running with odd numbers only produces this output:

Average Time: 178.05182ns

Average Skip Search Time: 263.82928ns

Running with your original random value produces this output:

Average Time: 175.95944ns

Average Skip Search Time: 181.20367ns

Each of these results makes sense.

When selecting even indices only, your skipSearch algorithm is O(n/2), so we're processing no more than half the indices. Normally we don't care about constant factors in time complexity, but if we're actually looking at the run-time, then it matters. We're literally cutting the problem in half in this case, so that's going to impact the execution time. And we see the real execution time is almost cut in half accordingly.

When selecting only odd indices, we see a much greater impact to execution time. This is to be expected, because we're processing no less than half the indices.

When the original random selection is used, we see skipSearch doing worse(as we expect). This is because, on average, we'll have an even number of even indices and odd indices. The even numbers will be found quickly, but the odd numbers will be found very slowly. The linear search will find index 3 early on, whereas the skipSearch processes roughly O(n/2) elements before it'll find index 3.

As to why your original code gives odd results, is up in the air as far as I'm concerned. It could be that the pseudo-random number generator slightly favors even numbers, it could be due to optimizations, it could be due to branch predictor madness. But it certainly wasn't comparing apples to apples by selecting random indices for both algorithms. Some of those things could still be affecting my results, but at least the two algorithms are trying to find the same numbers now.

Community
  • 1
  • 1
Shaz
  • 1,376
  • 8
  • 18
2

Both algorithms are doing the same, which one is faster depends on the place, where the value, you are looking for, is placed so it is coincidence, which one is faster in the ONE specific case.

But the first one is better coding style anyway.

Holger
  • 285,553
  • 42
  • 434
  • 765
tom3008
  • 100
  • 9
0

When people call linear search the "fastest search," it's a purely academic statement. It has nothing to do with benchmarks, but rather the Big O complexity of the search algorithm. To make this measurement useful, Big O only defines the worst case scenario for a given algorithm.

In the real world, data does not always adhere to the worst case scenario, so benchmarks will fluctuate for different data sets. In your example, there is a 7ns difference between the two algorithms. However, what would happen if your data looked like this:

linear_data = [..., value];
skip_search_data = [value, ...];

That 7ns difference would get a lot larger. For linear search, the complexity would be O(n) every time. For skip search it would be O(1) every time.

In the real world, the "fastest" algorithm isn't always the fastest. Sometimes, your dataset lends itself to a different algorithm.

Community
  • 1
  • 1
Darrick Herwehe
  • 3,553
  • 1
  • 21
  • 30