Robert's Data Science Blog

What started out as a simple migration to Julia 1.x for my package IntervalWavelets has now turned into a rewrite.

The idea is to focus much more on dyadic rationals, which are rational numbers whose denominator is a (non-negative) power of 2. That is, of the form $$k/2^r$$, where $$k$$ is an integer rand $$r$$ is a non-negative integer.

In Julia such a number can be represented with the following type:

struct DyadicRationals
numerator::Int64
scale::Int64
end


Instead of interacting directly with a struct I prefer to have “extractor” functions:



scale (generic function with 1 method)


Just like the rational numbers in Julia it makes sense to reduce the fraction as much as possible. To get started I just used ordinary reduction as for the built-in rationals:


g = gcd(numerator(x), denom(x))
num = div(numerator(x), g)
s = scale(x) - Int(log2(g))

end

reduce1 (generic function with 1 method)


reduce1(x)

Main.##WeaveSandBox#323.DyadicRationals(1, 1)


This works, but does not use the special form of the dyadic rational. Let’s see how fast this is using the BenchmarkTools package:


using BenchmarkTools
@benchmark reduce1($x)  BenchmarkTools.Trial: memory estimate: 0 bytes allocs estimate: 0 -------------- minimum time: 32.724 ns (0.00% GC) median time: 32.798 ns (0.00% GC) mean time: 34.209 ns (0.00% GC) maximum time: 108.003 ns (0.00% GC) -------------- samples: 10000 evals/sample: 991  Integer division is slow and so is the logarithm and type conversion. It should be possible to do this faster. To speed things up we need to get the largest power $$r$$ such that $$2^r$$ divides the numerator and denominator of the dyadic rational and then perform this division. To this end it turns out that Julia allows us to get work with the binary representation of an integer. Suppose the numerator has the binary representation $$k = \sum_{i = 0}^n a_i 2^i$$ For $$r > 0$$, $$k$$ is divisible by $$2^r$$ if $$a_0 = a_1 = \ldots = a_{r-1} = 0$$. This number of zero bits before the first non-zero bit is available in Julia with the function trailing_zeros. When $$r = 0$$ then $$a_0 = 1$$ and trailing_zeros still returns the correct answer. Furthermore, division by a power of 2 is the so-called arithmetic shift that Julia offer as >>. A reduce function with these trick looks like this:  function reduce2(x::DyadicRationals) numerator_power_two = trailing_zeros(numerator(x)) common_power_two = min(scale(x), numerator_power_two) num = numerator(x) >> common_power_two s = scale(x) - common_power_two DyadicRationals(num, s) end  reduce2 (generic function with 1 method)  Let’s see how fast this is.  @benchmark reduce2($(Ref(x))[] )

BenchmarkTools.Trial:
memory estimate:  0 bytes
allocs estimate:  0
--------------
minimum time:     1.925 ns (0.00% GC)
median time:      2.244 ns (0.00% GC)
mean time:        2.511 ns (0.00% GC)
maximum time:     17.758 ns (0.00% GC)
--------------
samples:          10000
evals/sample:     1000


More than an order of magnitude faster! The weird way of passing x to reduce2 is a to a tip from BenchmarkTools' README, because reduce2 is simple function. if reduce2 was called with \$x we would see timings that were yet another order of magnitude faster.

## Weave

This post is also my first attempt at using the Weave package that generates the Markdown and evaluates the Julia code.