22

Given an array having .length 100 containing elements having values 0 to 99 at the respective indexes, where the requirement is to find element of of array equal to n : 51.

Why is using a loop to iterate from start of array to end faster than iterating both start to end and end to start?

const arr = Array.from({length: 100}, (_, i) => i);
const n = 51;
const len = arr.length;

console.time("iterate from start");
for (let i = 0; i < len; i++) {
  if (arr[i] === n) break;
}
console.timeEnd("iterate from start");

const arr = Array.from({length: 100}, (_, i) => i);
const n = 51;
const len = arr.length;

console.time("iterate from start and end");
for (let i = 0, k = len - 1; i < len && k >= 0; i++, k--) {
  if (arr[i] === n || arr[k] === n) break;
}
console.timeEnd("iterate from start and end");

jsperf https://jsperf.com/iterate-from-start-iterate-from-start-and-end/1

guest271314
  • 1
  • 15
  • 104
  • 177
  • going to have to ask if you've done this in more than just a single browser :p you do realise you have a lot more happening in each iteration of the second code, right? an extra check `>=` and an `||` and another `===` – Jaromanda X Nov 01 '17 at 05:53
  • @JaromandaX Yes. Chromium and Firefox have same result as to iterating from start to end is fastest. The expected result is for start to end and end to start to be faster than only start to end as `arr[k] === n` should be reached before `arr[i] === n`, it is less steps from `99` to `51` than from `0` to `51`; `51` and `48` respectively – guest271314 Nov 01 '17 at 05:57
  • 1
    still, a bit more happening in each iteration, perhaps can't be as easily "optimised" on the fly? – Jaromanda X Nov 01 '17 at 05:58
  • @JaromandaX Is additional time needed for the second object lookup? Should there be two separate loops for each start to end and end to start? Or is that still the same code in two different blocks? – guest271314 Nov 01 '17 at 06:00
  • 1
    dude, I'm the last person to really ask about benchmarking and the innards of stuff like this - if something takes 101ms instead of 80ms, I really don't care :p – Jaromanda X Nov 01 '17 at 06:01
  • @JaromandaX The question is presented as to programming, though the concept is extensible to the inquiry into does the necessary time and resources to observe an object from two perspectives require more time and resources than viewing an object from a single perspective; even where one perspective is closer to the required intersection of observation than the adjacent observer to the required intersection? Or, put differently still, what is the fastest approach to find an element in an array? – guest271314 Nov 01 '17 at 06:04
  • @JaromandaX Yet another approach which did not include at code at Question is to randomize the indexes of the array in an array and iterate those indexes to note the different outcomes; to determine how, over time, random selection compares to start to end, and start to end with end to start. – guest271314 Nov 01 '17 at 06:11
  • I cannot reproduce. What browser do you use, and how many iterations have you tried? After several runs, the fastest for the first snippet was `0.030ms` while the fastest for the second snippet was `0.040ms`. – Bergi Nov 08 '17 at 05:49
  • Your question title conflicts with your question text. The one asks "*Why is using a loop to iterate from start of array to end **faster** than iterating both start to end and end to start?*" whereas the other asks "*why is using a loop to iterate from start to end **slower** than iterating from both start to end and end to start?*"! What are your results, and why is that unexpected? – Bergi Nov 08 '17 at 05:52
  • @Bergi What do you not understand about the question? – guest271314 Nov 08 '17 at 06:00
  • @guest271314 I understand both questions quite clearly, it's just that they are completely different questions. – Bergi Nov 08 '17 at 06:01
  • @Bergi There is only one question. You understand the one question. There is no confusion as to what the one question is asking. – guest271314 Nov 08 '17 at 06:02
  • @guest271314 You state both that it's faster and that it's slower. That's not possible. Which is it? Please fix one of the sentences, and also include the measurement values that you are getting. – Bergi Nov 08 '17 at 06:05
  • @Bergi Trying to iterate both from start to end and from end to start is slower than iterating from start to end exclusively. – guest271314 Nov 08 '17 at 06:09
  • @Bergi See updated post. The relevant jsperf is linked at OP. The code at stacksnippets includes use of `console.time()`. If different results are reached by viewers of the question who run the code they can convey the same at comment and or answer. – guest271314 Nov 08 '17 at 06:11
  • for (let i = 0, k = len - 1; i < (len / 2) && k >= (len / 2); i++, k--) should take the same amount of time because you aren't iterating the entire thing for k and i. This one iterates half the array with i and the other half with k, in opposite directions, meeting in the middle. – Brandon Miller Nov 14 '17 at 20:20

5 Answers5

42

The answer is pretty obvious:

More operations take more time.

When judging the speed of code, you look at how many operations it will perform. Just step through and count them. Every instruction will take one or more CPU cycles, and the more there are the longer it will take to run. That different instructions take a different amount of cycles mostly does not matter - while an array lookup might be more costly than integer arithmetic, both of them basically take constant time and if there are too many, it dominates the cost of our algorithm.

In your example, there are few different types of operations that you might want to count individually:

  • comparisons
  • increments/decrements
  • array lookup
  • conditional jumps

(we could be more granular, such as counting variable fetch and store operations, but those hardly matter - everything is in registers anyway - and their number basically is linear to the others).

Now both of your code iterate about 50 times - they element on which they break the loop is in the middle of the array. Ignoring off-by-a-few errors, those are the counts:

               |  forwards  | forwards and backwards
---------------+------------+------------------------
>=/===/<       |       100  |                   200
++/--          |        50  |                   100
a[b]           |        50  |                   100
&&/||/if/for   |       100  |                   200

Given that, it's not unexpected that doing twice the works takes considerably longer.

I'll also answer a few questions from your comments:

Is additional time needed for the second object lookup?

Yes, every individual lookup counts. It's not like they could be performed at once, or optimised into a single lookup (imaginable if they had looked up the same index).

Should there be two separate loops for each start to end and end to start?

Doesn't matter for the number of operations, just for their order.

Or, put differently still, what is the fastest approach to find an element in an array?

There is no "fastest" regarding the order, if you don't know where the element is (and they are evenly distributed) you have to try every index. Any order - even random ones - would work the same. Notice however that your code is strictly worse, as it looks at each index twice when the element is not found - it does not stop in the middle.

But still, there are a few different approaches at micro-optimising such a loop - check these benchmarks.

starball
  • 20,030
  • 7
  • 43
  • 238
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 1
    I added another revision of your JSPerf tests showing some unrolled examples which generally outperform normal loops: https://jsperf.com/iterate-from-start-iterate-from-start-and-end/9 Mind you, there's this to understand: https://stackoverflow.com/questions/38111355/javascript-are-loops-faster-than-discretely-writing-line-by-line – dnickless Nov 14 '17 at 21:03
2

@Bergi is correct. More operations is more time. Why? More CPU clock cycles. Time is really a reference to how many clock cycles it takes to execute the code. In order to get to the nitty-gritty of that you need to look at the machine level code (like assembly level code) to find the true evidence. Each CPU (core?) clock cycle can execute one instruction, so how many instructions are you executing?

I haven't counted the clock cycles in a long time since programming Motorola CPUs for embedded applications. If your code is taking longer then it is in fact generating a larger instruction set of machine code, even if the loop is shorter or runs an equal amount of times.

Never forget that your code is actually getting compiled into a set of commands that the CPU is going to execute (memory pointers, instruction-code level pointers, interrupts, etc.). That is how computers work and its easier to understand at the micro controller level like an ARM or Motorola processor but the same is true for the sophisticated machines that we are running on today.

Your code simply does not run the way you write it (sounds crazy right?). It is run as it is compiled to run as machine level instructions (writing a compiler is no fun). Mathematical expression and logic can be compiled in to quite a heap of assembly, machine level code and that is up to how the compiler chooses to interpret it (it is bit shifting, etc, remember binary mathematics anyone?)

Reference: https://software.intel.com/en-us/articles/introduction-to-x64-assembly

Your question is hard to answer but as @Bergi stated the more operations the longer, but why? The more clock cycles it takes to execute your code. Dual core, quad core, threading, assembly (machine language) it is complex. But no code gets executed as you have written it. C++, C, Pascal, JavaScript, Java, unless you are writing in assembly (even that compiles down to machine code) but it is closer to actual execution code.

A masters in CS and you will get to counting clock cycles and sort times. You will likely make you own language framed on machine instruction sets.

Most people say who cares? Memory is cheap today and CPUs are screaming fast and getting faster.

But there are some critical applications where 10 ms matters, where an immediate interrupt is needed, etc.

Commerce, NASA, a Nuclear power plant, Defense Contractors, some robotics, you get the idea . . .

I vote let it ride and keep moving.

Cheers, Wookie

1

Since the element you're looking for is always roughly in the middle of the array, you should expect the version that walks inward from both the start and end of the array to take about twice as long as one that just starts from the beginning.

Each variable update takes time, each comparison takes time, and you're doing twice as many of them. Since you know it will take one or two less iterations of the loop to terminate in this version, you should reason it will cost about twice as much CPU time.

This strategy is still O(n) time complexity since it only looks at each item once, it's just specifically worse when the item is near the center of the list. If it's near the end, this approach will have a better expected runtime. Try looking for item 90 in both, for example.

Justin Summerlin
  • 4,938
  • 1
  • 16
  • 10
1

Selected answer is excellent. I'd like to add another aspect: Try findIndex(), it's 2-3 times faster than using loops:

const arr = Array.from({length: 900}, (_, i) => i);
const n = 51;
const len = arr.length;

console.time("iterate from start");
for (let i = 0; i < len; i++) {
  if (arr[i] === n) break;
}
console.timeEnd("iterate from start");

console.time("iterate using findIndex");
var i = arr.findIndex(function(v) {
  return v === n;
});
console.timeEnd("iterate using findIndex");
oriadam
  • 7,747
  • 2
  • 50
  • 48
0

The other answers here cover the main reasons, but I think an interesting addition could be mentioning cache.

In general, sequentially accessing an array will be more efficient, particularly with large arrays. When your CPU reads an array from memory, it also fetches nearby memory locations into cache. This means that when you fetch element n, element n+1 is also probably loaded into cache. Now, cache is relatively big these days, so your 100 int array can probably fit comfortably in cache. However, on an array of much larger size, reading sequentially will be faster than switching between the beginning and the end of the array.

VivaLaPanda
  • 809
  • 7
  • 24