Yesterday, I stumbled across this couple-month old blog post from Josiah Parry walking through creating R, Rust, and C++ functions to compare multiple candidate strings to a reference string (his real-world application for this is geohashing, but in the demo he uses arbitrary strings).
Those languages are cool and all, but what about Julia? The gist of his blog is that Rust is super fast. And since the whole, like raison d’etre of Julia is that it’s fast, I figured I’d write a version of this in Julia as well. I’m still new-ish to Julia, so I’d love if any experts could tell me how to optimize this even further.
Load Packages and Generate Data
For this, we just need the Random package to set a seed and sample our strings as well as the BenchmarkTools package to benchmark the function performance.
usingRandomusingBenchmarkToolsRandom.seed!(0408)#function to generate some stringsfunctionmake_strings(n::Int) v =Vector{String}(undef, n) letters ="abcde" numbers ="12345"for i ∈eachindex(v) x =randstring(letters, 4) y =randstring(numbers, 3) v[i] = x * yendreturn vend
make_strings (generic function with 1 method)
This will make a vector of length n where each element is a 7-character string. In each of these strings, the first 4 characters will be sampled (with replacement) from "abcde", and the last 3 characters will be sampled (with replacement) from "12345".
Next we’ll set n to 100,000 and generate our strings. I’ll also make an arbitrary reference string to compare the candidate strings against. Note that we’re not benchmarking any of this stuff – just the comparisions that will come later.
n =100_000#returns a vector of 100k stringsx =make_strings(n);ref ="aade124"#making a reference string to compare against
"aade124"
Write Comparison Functions
So now we want to compare each element of x to ref. The goal is count how many characters match until we hit the first characters that don’t match. For example, if we’re comparing "abcd123" to "abde123", the result would be 2, since the first two characters (ab vs ab) match in each, but the third characters (c vs d) don’t.
My first step here is to write a function that compares 1 string to 1 string – that is’, I’m not worrying about the fact that I want to do this for all the of the elements in x yet – I just want to do it for 1 element.
functioncompare_strings(x::String, y::String) s =0for i ∈eachindex(x) x[i] != y[i] ? break: s +=1endreturn send
compare_strings (generic function with 1 method)
This will: 1. Create a counter, s (for sum) and set it equal to 0; 2. For each index i (position) in x – recall that x and y will have the same length – compare x[i] and y[i]; 3. If they’re not equal, break the loop and return s; 4. If they are equal, increment s by one and keep going
We can check that this works by using the previous example strings:
compare_strings("abcd123", "abde123")
2
Now we want to write a version of this function that accepts a vector of strings and compares each element of that vector to the reference string. The cool thing about Julia is that its multiple dispatch feature allows us to define another compare_strings() function that accepts different types of arguments.
So we can write the following and it’s perfectly acceptable and, honestly, way better IMO than how you might have to handle this in R or python
functioncompare_strings(x::Vector{String}, y::String)return [compare_strings(i, y) for i in x]end
compare_strings (generic function with 2 methods)
Notice that the new function has the same name (compare_strings()) but its x argument is a vector of strings rather than a single string. Then, inside the function, we just call our other method that requires x to be a single string. We do these calls inside of a list comprehension to iterate over all of the elements in x.
Benchmark
Now we just run the benchmark to see how our code does
@benchmarkcompare_strings(x, ref)
BenchmarkTools.Trial: 5038 samples with 1 evaluation.
Range (min … max): 880.500 μs … 2.713 ms┊ GC (min … max): 0.00% … 26.16%
Time (median): 951.200 μs ┊ GC (median): 0.00%
Time (mean ± σ): 984.414 μs ± 115.531 μs┊ GC (mean ± σ): 0.87% ± 4.08%
▁▆██▄▂
▂▅██████▇▇▆▅▅▄▃▃▃▃▃▃▃▃▃▃▂▂▂▂▂▂▂▂▂▂▂▂▂▁▂▁▁▂▂▂▁▁▂▂▂▂▁▂▂▂▂▂▂▂▂▂▂ ▃
880 μs Histogram: frequency by time 1.58 ms <
Memory estimate: 781.30 KiB, allocs estimate: 2.
Obviously this isn’t an apples-to-apples comparison with the code Josiah wrote – we have different machines, different input vectors, he was calling both Rust and C++ from R, etc. But the point remains that Julia is also fast…just in case people hadn’t heard :)