4 ms·
> For most code, you don't know how fast it "ought to" be This is exactly it. I was working on a "fastest substring search" challenge back in 2016 for some pri
by jorangreef 6y ago
> For most code, you don't know how fast it "ought to" be
This is exactly it. I was working on a "fastest substring search" challenge back in 2016 for some prize money and I was trying to think about the methodology I should use to win the competition.
I realized that the biggest problem was not actually the problem of optimizing the substring search defined by the competition, but rather knowing when my entry would almost certainly be faster than the other entries.
After that, the next biggest problems were still not the substring problem, but the types of machine the competition organizers would benchmark on, and the various sizes of the input data they would use.
I decided to start with the practical limit and then work backwards from there. In other words, measure something almost like an identity function followed by a simple linear for loop buffer scan, and then use this "null-cipher" limit as my competitor, because there's nothing faster than doing almost no work.
All this lead away from theoretically optimal algorithms such as Aho–Corasick towards something based on Rabin-Karp that could also switch dynamically to a second fallback brute force algorithm optimized for small data, depending on the length of the input data. There was also a curve of precomputed branch points in the algorithm to know when to switch, calculated according to the type of machine the organizers would use, probably server grade rather desktop.
Learning this lesson was probably the most important so far for me. These days when I start with a design problem, I figure out what the "null-cipher" performance would be (this kind of number is usually fairly cheap to obtain: like the speed of light in a vacuum if you were optimizing fiber links, or the max hello world http requests per second for a typical server), and then use back-of-the-envelope numbers to work backwards from there to know where the implementation should reasonably be expected to land up if there are no performance bugs. This way you know you've got a winner.
This is the exact opposite to starting with something and then trying to make it faster, and I find it saves wasted debugging time by avoiding implementations that can never be made fast enough for the requirements.
- tayo42 6y agoDid you win the contest?
- jorangreef 6y agoYes :) By a hair of 1ms on the sprint data set (76ms vs 77ms), but by 2x on the long distance marathon data set, compared to the next runner up, who also used a switching hybrid to enter two types of "runner" in terms of algorithm, but with a single switch point instead of a curve so the sprint algorithm wasn't always used when it should have been, and a slower long distance algorithm that used a more theoretically optimal algorithm but with pointer chasing and more cache misses on real hardware. This was also in JavaScript, on a literal V8 input data string instead of a Node.js buffer, and the thing that really helped was taking time upfront to first recode the string to a buffer. Sounds crazy and slow and unintuitive, but byte operations on a buffer are way faster than charCodeAt() function calls on a string and this more than made up for the upfront recoding. This came the closest to the "null cipher" imaginary competitor I had, close enough within a few percent, of all the prototypes I tried, so I submitted...