2015-07-27

Browsermark: Array-Weighted

Let's follow Array-Blur's "next_test" link, which leads us to http://web.basemark.com/tests/2.1/benchmarks/javascript/array_weighted/test.js. Like before, running this code through a beautifier renders it much more readable.

This is a great example for completely artificial code: it doesn't do anything useful, or compute any result, it just plays around with strings and arrays like a kid in a sandbox, forming a shape and destroying it to form another, and so on. See for yourself:

First we have "countries", an array of strings, where each string is a comma-separated list of country names. The fact that they're country names is completely irrelevant, they might as well be random strings.
var countries = [
  "Afghanistan,Albania,...",
  "Bahamas,Bahrain,...",
  ...]
Now for the test's main and only function:
run: function(stop_now, iteration_index) {
Pick an index into the "countries" array...
  var e = iteration_index % countries.length;
...retrieve the string there...
  var o = countries[e];
...and split it at its commas, so "d" is now an array of country names.
  var d = o.split(",");
Initialize two empty arrays "k" and "j", and temporary variable h, which is initialized to the empty string, but that's irrelevant because it will get overwritten two lines down anyway.
  var k = [];
  var j = [];
  var h = "";
Now they use jQuery's each function to iterate over the array "d". As jQuery's documentation describes in detail, this function takes an array and a callback as arguments, and will invoke the callback for every element of the array, passing in the current index and the element at that index. So typically that would look like: $.each(array, function(index, element) { do_something_with(element); });. Of course, with JavaScript not having any binding rules, they can choose to ignore this advice:
  $.each(d, function() {
And instead of taking an "element" argument in the callback, pop the first element off the array. Note that this is really expensive, because the JavaScript engine must move all remaining elements forward to fill the gap.
    h = d.shift();
Next, they copy out the first two and the first three characters of the country name:
    c2 = h.slice(0, 2);
    c3 = h.slice(0, 3);
And append them to either of the two arrays created above, if it doesn't contain the new entry yet. Note that this, again, is fairly expensive, because ".indexOf" must look at every existing element of the array.
    if (k.indexOf(c2) == -1) {
      k.push(c2)
    }
    if (j.indexOf(c3) == -1) {
      j.push(c3)
    }
Finally, the element that was previously popped off the array's beginning is appended back to its end, so that after all iterations have completed, the array will be exactly like it was before.
    d.push(h)
  });
Now the lists of 2-character and 3-character prefixes are copied into a new array containing both their contents:
  var c = k.concat(j);
The resulting list is then sorted. It is unclear why this is being done. Probably because we can.
  c.sort();
Then an index into the sorted prefix list is chosen.
  var g = iteration_index % c.length;
And a reverse-ordered copy of the list of country names is created.
  var m = d.reverse();
Someone felt it would be appropriate to have another temporary variable here, initialized to the empty string, although again this is irrelevant, as the value is never used for anything.
  var a = "";
Now there's one more jQuery-powered iteration, again without the callback's typical arguments...
  $.each(m, function() {
...but just to spice things up, this time around, they pop elements off the array's end. Having reversed it before, this results in the same order of processed elements as before. Which doesn't even matter for the code at hand. Whatever. The string is also split into an array of individual characters. Because you can do that with strings.
    h = m.pop().split("");
"c[g]" is a randomly selected prefix, "p" is its length (either 2 or 3).
    var p = c[g].length;
Now they could just have used "h.slice(0, p)" like above, but why go the fast and elegant route when you can take a slow detour? Having split the country name into an array of single characters two lines above, they can now use the array's "splice()" function to remove the first "p" elements and put them into a new array, which is then joined with empty separators, i.e. turned into a string again. Confused yet?
    var i = h.splice(0, p).join("");
This so laboriously computed prefix of the country name is now compared to the randomly selected prefix, and if they are equal...
    if (i == c[g].toString()) {
...then the rest of the country name is converted back to a string and appended to the prefix. Which gives us back the full country name. It's identical to the result of "m.pop()" above. Cool, we've run in a circle. Except nobody cares. The result is just assigned to the variable "a", which nobody will ever look at.
      a = i + h.join("")
    }
  });
That's it! All done! And since the test has just done maybe about roughly 13-ish things or so, the counter should obviously be incremented by 13. Duh!
  counter += 13;

I still haven't found the "weights" that the test's name "Array-Weighted" led me to believe there would be.

As far as I can see, it tests Array.splice, Array.shift, Array.sort, Array.concat, and String.split on completely contrived examples. I can see how benchmarking such library functions could be considered reasonable; on the other hand all these functions are necessarily fairly expensive (just because of what they do, by definition), so code that cares about performance would typically avoid using them anyway, which in my view drastically reduces the practical relevance of this benchmark.

Here's another interesting observation: this test runs its main function (the one we analyzed above) as often as it can until 4 seconds have elapsed. Generally, that's a relatively sound approach to benchmarking, at least if you intend to measure maximum throughput. However, even though what the function does is fairly complex and inefficient, it's still so fast that checking whether 4 seconds have elapsed yet after every iteration takes more than 4% of the entire benchmark's time. How do I know this? Easy: I've modified the benchmark runner to do 1000 iterations of the test before checking again how much time has passed. Doing so increased the score by more than 4% (averaged over 5 runs).
Sure, you can say that 4% isn't a huge deal. However it is clearly more overhead than I would expect from a benchmark made by a company who claims to be experts at benchmarking. Ask a professional NASCAR or Formula One driver what they would say if whatever system measured their time per lap also slowed them down by 4%!

Conclusion:
I've tried to understand what this test is good for, or what typical use-case it is representative of. I've tried really hard. I couldn't. It just seems pointless.

No comments:

Post a Comment