carray-jit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +84 -0
- data/LICENSE +21 -0
- data/README.md +88 -0
- data/bin/carray-jit +194 -0
- data/carray-jit.gemspec +41 -0
- data/docs/00_Introduction.md +40 -0
- data/docs/01_GettingStarted.md +80 -0
- data/docs/02_KernelShapes.md +397 -0
- data/docs/03_SupportedFeatures.md +595 -0
- data/docs/04_Compiling.md +234 -0
- data/docs/05_DesignNotes.md +136 -0
- data/docs/06_Cheatsheet.md +177 -0
- data/examples/README.md +56 -0
- data/examples/applications/game_of_life.rb +161 -0
- data/examples/applications/heat_equation.rb +117 -0
- data/examples/applications/kepler.rb +178 -0
- data/examples/applications/mandelbrot.rb +151 -0
- data/examples/applications/moving_average.rb +124 -0
- data/examples/applications/partial_sums.rb +141 -0
- data/examples/applications/point_cloud.rb +110 -0
- data/examples/applications/quicksort.rb +118 -0
- data/examples/applications/recursion.rb +121 -0
- data/examples/applications/relaxation.rb +115 -0
- data/examples/applications/sensor_gaps.rb +118 -0
- data/examples/applications/sieve.rb +95 -0
- data/examples/applications/sobel_edges.rb +80 -0
- data/examples/features/01_element_wise.rb +69 -0
- data/examples/features/02_stencil.rb +40 -0
- data/examples/features/03_recurrence.rb +50 -0
- data/examples/features/04_thomas.rb +81 -0
- data/examples/features/05_reduction.rb +90 -0
- data/examples/features/06_jit_contract.rb +58 -0
- data/examples/features/07_masks.rb +55 -0
- data/examples/features/08_views.rb +46 -0
- data/examples/features/09_inspecting.rb +55 -0
- data/examples/features/10_complex.rb +107 -0
- data/examples/features/11_c_functions.rb +260 -0
- data/examples/features/12_sweep.rb +139 -0
- data/examples/features/13_cscalar.rb +80 -0
- data/examples/features/14_stencil_window.rb +106 -0
- data/examples/features/15_loops.rb +148 -0
- data/examples/features/16_raising.rb +69 -0
- data/ext/carray_jit_access/carray_jit_access.c +460 -0
- data/ext/carray_jit_access/extconf.rb +8 -0
- data/lib/carray/jit/analyzer.rb +1847 -0
- data/lib/carray/jit/block_reader.rb +139 -0
- data/lib/carray/jit/c_function.rb +777 -0
- data/lib/carray/jit/c_generator.rb +2305 -0
- data/lib/carray/jit/compiler.rb +468 -0
- data/lib/carray/jit/errors.rb +37 -0
- data/lib/carray/jit/expression.rb +202 -0
- data/lib/carray/jit/kernel.rb +509 -0
- data/lib/carray/jit/node.rb +573 -0
- data/lib/carray/jit/sweep.rb +97 -0
- data/lib/carray/jit/type_assignment.rb +811 -0
- data/lib/carray/jit/version.rb +5 -0
- data/lib/carray/jit.rb +1210 -0
- metadata +139 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Smoothing a time series, and measuring the drawdown
|
|
2
|
+
#
|
|
3
|
+
# These are the calculations CArray's operators cannot help with, because each
|
|
4
|
+
# value depends on the one before it: an exponential moving average, a running
|
|
5
|
+
# peak, the drawdown from that peak. There is no way to write them as an
|
|
6
|
+
# expression over whole arrays -- so ordinarily you write a Ruby loop and pay
|
|
7
|
+
# for a block call per sample.
|
|
8
|
+
#
|
|
9
|
+
# ruby examples/applications/moving_average.rb
|
|
10
|
+
|
|
11
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
12
|
+
require "carray/jit"
|
|
13
|
+
|
|
14
|
+
n = 500_000
|
|
15
|
+
random = Random.new(20260902)
|
|
16
|
+
|
|
17
|
+
# A price series: a multiplicative random walk with a slow drift.
|
|
18
|
+
price = CArray.double(n)
|
|
19
|
+
value = 100.0
|
|
20
|
+
n.times do |i|
|
|
21
|
+
value *= 1.0 + random.rand(-0.004..0.004) + 0.000002 * Math.sin(i / 5000.0)
|
|
22
|
+
price[i] = value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
alpha = 0.02
|
|
26
|
+
smoothed = CArray.double(n)
|
|
27
|
+
peak = CArray.double(n)
|
|
28
|
+
drawdown = CArray.double(n)
|
|
29
|
+
|
|
30
|
+
smoothed[0] = price[0]
|
|
31
|
+
peak[0] = price[0]
|
|
32
|
+
|
|
33
|
+
# Exponential moving average. One line, and the dependency on the previous
|
|
34
|
+
# cell is exactly what the extent's direction records.
|
|
35
|
+
CArray.jit_for(1...n) { |i|
|
|
36
|
+
smoothed[i] = alpha * price[i] + (1.0 - alpha) * smoothed[i-1]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# The running peak and the drawdown from it -- a branch per cell, which is
|
|
40
|
+
# also why this is not an expression over arrays.
|
|
41
|
+
CArray.jit_for(1...n) { |i|
|
|
42
|
+
if price[i] > peak[i-1]
|
|
43
|
+
peak[i] = price[i]
|
|
44
|
+
else
|
|
45
|
+
peak[i] = peak[i-1]
|
|
46
|
+
end
|
|
47
|
+
drawdown[i] = (price[i] - peak[i]) / peak[i]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
worst = drawdown.min
|
|
51
|
+
puts "#{n} samples"
|
|
52
|
+
puts format(" last price %.2f", price[n-1])
|
|
53
|
+
puts format(" smoothed %.2f", smoothed[n-1])
|
|
54
|
+
puts format(" worst drawdown %.2f%% at sample %d", worst * 100, (drawdown.eq(worst)).where[0])
|
|
55
|
+
|
|
56
|
+
# A rolling mean, as a cumulative sum and its own difference `window` cells
|
|
57
|
+
# back. The window is an ordinary local: an offset may be an integer the
|
|
58
|
+
# block closed over, and it reaches the kernel as an argument, so changing the
|
|
59
|
+
# window does not compile anything again.
|
|
60
|
+
window = 200
|
|
61
|
+
cumulative = CArray.double(n)
|
|
62
|
+
rolling = CArray.double(n)
|
|
63
|
+
cumulative[0] = price[0]
|
|
64
|
+
CArray.jit_for(1...n) { |i| cumulative[i] = cumulative[i-1] + price[i] }
|
|
65
|
+
CArray.jit_for(window...n) { |i|
|
|
66
|
+
rolling[i] = (cumulative[i] - cumulative[i - window]) / window
|
|
67
|
+
}
|
|
68
|
+
puts format(" rolling(%d) %.2f", window, rolling[n-1])
|
|
69
|
+
|
|
70
|
+
# The same three calculations as the Ruby loops they replace.
|
|
71
|
+
def ruby_versions (price, alpha, n)
|
|
72
|
+
smoothed = Array.new(n, 0.0)
|
|
73
|
+
peak = Array.new(n, 0.0)
|
|
74
|
+
drawdown = Array.new(n, 0.0)
|
|
75
|
+
smoothed[0] = price[0]
|
|
76
|
+
peak[0] = price[0]
|
|
77
|
+
(1...n).each do |i|
|
|
78
|
+
smoothed[i] = alpha * price[i] + (1.0 - alpha) * smoothed[i-1]
|
|
79
|
+
peak[i] = price[i] > peak[i-1] ? price[i] : peak[i-1]
|
|
80
|
+
drawdown[i] = (price[i] - peak[i]) / peak[i]
|
|
81
|
+
end
|
|
82
|
+
[smoothed, peak, drawdown]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
86
|
+
reference_smoothed, _, reference_drawdown = ruby_versions(price, alpha, n)
|
|
87
|
+
interpreted = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
88
|
+
|
|
89
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
90
|
+
CArray.jit_for(1...n) { |i|
|
|
91
|
+
smoothed[i] = alpha * price[i] + (1.0 - alpha) * smoothed[i-1]
|
|
92
|
+
}
|
|
93
|
+
CArray.jit_for(1...n) { |i|
|
|
94
|
+
if price[i] > peak[i-1]
|
|
95
|
+
peak[i] = price[i]
|
|
96
|
+
else
|
|
97
|
+
peak[i] = peak[i-1]
|
|
98
|
+
end
|
|
99
|
+
drawdown[i] = (price[i] - peak[i]) / peak[i]
|
|
100
|
+
}
|
|
101
|
+
compiled = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
102
|
+
|
|
103
|
+
puts format(" %.1f ms in Ruby, %.1f ms compiled (%.0fx)",
|
|
104
|
+
interpreted * 1e3, compiled * 1e3, interpreted / compiled)
|
|
105
|
+
puts " same answers to the last bit: #{smoothed.to_a == reference_smoothed &&
|
|
106
|
+
drawdown.to_a == reference_drawdown}"
|
|
107
|
+
|
|
108
|
+
# The series and its moving average, sampled at a hundred points.
|
|
109
|
+
puts
|
|
110
|
+
low, high = price.min, price.max
|
|
111
|
+
levels = 14
|
|
112
|
+
scale = ->(v) { ((v - low) / (high - low) * levels).round }
|
|
113
|
+
raw = (0...100).map { |k| price[k * (n / 100)] }
|
|
114
|
+
ema = (0...100).map { |k| smoothed[k * (n / 100)] }
|
|
115
|
+
levels.downto(0) do |level|
|
|
116
|
+
row = (0...100).map { |k|
|
|
117
|
+
if scale.(ema[k]) == level then "-"
|
|
118
|
+
elsif scale.(raw[k]) == level then "."
|
|
119
|
+
else " "
|
|
120
|
+
end
|
|
121
|
+
}
|
|
122
|
+
puts " " + row.join
|
|
123
|
+
end
|
|
124
|
+
puts " price ., moving average -"
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Nine series summed in one pass -- the "partial sums" benchmark, as it is
|
|
2
|
+
# written for an interpreter: one loop over d = 1..n, nine accumulators, and
|
|
3
|
+
# no array anywhere in it.
|
|
4
|
+
#
|
|
5
|
+
# A kernel can be that program. The accumulators are CScalars, which are the
|
|
6
|
+
# one-cell arrays they subclass, and every iteration writing the one cell they
|
|
7
|
+
# have is what makes an accumulator without asking for one.
|
|
8
|
+
#
|
|
9
|
+
# And the answers are the Ruby loop's, bit for bit, with nothing asked for:
|
|
10
|
+
# the kernel's own loop over its extents is taken in the order the extents
|
|
11
|
+
# say. What may be split into partial sums -- the name of this file -- is an
|
|
12
|
+
# *inner* loop, the reduction a cell does for itself, and the second half of
|
|
13
|
+
# this program is the same harmonic sum written that way, where the order does
|
|
14
|
+
# become a choice.
|
|
15
|
+
#
|
|
16
|
+
# ruby examples/applications/partial_sums.rb
|
|
17
|
+
|
|
18
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
19
|
+
require "carray/jit"
|
|
20
|
+
|
|
21
|
+
N = 500_000
|
|
22
|
+
|
|
23
|
+
def accumulators
|
|
24
|
+
Array.new(9) { CScalar.double() { 0.0 } }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def sum_series (n)
|
|
28
|
+
s0, s1, s2, s3, s4, s5, s6, s7, s8 = accumulators
|
|
29
|
+
CArray.jit_for(1..n) { |k|
|
|
30
|
+
d = k * 1.0
|
|
31
|
+
d2 = d * d
|
|
32
|
+
d3 = d2 * d
|
|
33
|
+
ds = Math.sin(d)
|
|
34
|
+
dc = Math.cos(d)
|
|
35
|
+
# The alternating sign is written from the index rather than carried in a
|
|
36
|
+
# variable: `alt = -alt` would be a chain from one iteration to the next,
|
|
37
|
+
# and this is the same sequence with no chain in it.
|
|
38
|
+
alt = k % 2 == 1 ? 1.0 : -1.0
|
|
39
|
+
|
|
40
|
+
s0[] = s0[] + (2.0 / 3.0) ** (d - 1.0) # a geometric series
|
|
41
|
+
s1[] = s1[] + 1.0 / Math.sqrt(d) # zeta(1/2), which diverges
|
|
42
|
+
s2[] = s2[] + 1.0 / (d * (d + 1.0)) # telescoping, to 1
|
|
43
|
+
s3[] = s3[] + 1.0 / (d3 * ds * ds) # Flint Hills
|
|
44
|
+
s4[] = s4[] + 1.0 / (d3 * dc * dc) # Cookson Hills
|
|
45
|
+
s5[] = s5[] + 1.0 / d # harmonic
|
|
46
|
+
s6[] = s6[] + 1.0 / d2 # zeta(2)
|
|
47
|
+
s7[] = s7[] + alt / d # alternating harmonic
|
|
48
|
+
s8[] = s8[] + alt / (2.0 * d - 1.0) # Gregory's series
|
|
49
|
+
}
|
|
50
|
+
[s0, s1, s2, s3, s4, s5, s6, s7, s8].map { |s| s[0] }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def sum_series_in_ruby (n)
|
|
54
|
+
s0 = s1 = s2 = s3 = s4 = s5 = s6 = s7 = s8 = 0.0
|
|
55
|
+
d_int = 1
|
|
56
|
+
while d_int <= n
|
|
57
|
+
d = d_int.to_f
|
|
58
|
+
d2 = d * d
|
|
59
|
+
d3 = d2 * d
|
|
60
|
+
ds = Math.sin(d)
|
|
61
|
+
dc = Math.cos(d)
|
|
62
|
+
alt = d_int % 2 == 1 ? 1.0 : -1.0
|
|
63
|
+
s0 = s0 + (2.0 / 3.0) ** (d - 1.0)
|
|
64
|
+
s1 = s1 + 1.0 / Math.sqrt(d)
|
|
65
|
+
s2 = s2 + 1.0 / (d * (d + 1.0))
|
|
66
|
+
s3 = s3 + 1.0 / (d3 * ds * ds)
|
|
67
|
+
s4 = s4 + 1.0 / (d3 * dc * dc)
|
|
68
|
+
s5 = s5 + 1.0 / d
|
|
69
|
+
s6 = s6 + 1.0 / d2
|
|
70
|
+
s7 = s7 + alt / d
|
|
71
|
+
s8 = s8 + alt / (2.0 * d - 1.0)
|
|
72
|
+
d_int = d_int + 1
|
|
73
|
+
end
|
|
74
|
+
[s0, s1, s2, s3, s4, s5, s6, s7, s8]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
NAMES = ["(2/3)^(d-1)", "1/sqrt(d)", "1/(d(d+1))", "1/(d^3 sin^2 d)",
|
|
78
|
+
"1/(d^3 cos^2 d)", "1/d", "1/d^2", "(-1)^(d-1)/d",
|
|
79
|
+
"(-1)^(d-1)/(2d-1)"]
|
|
80
|
+
|
|
81
|
+
sums = sum_series(N)
|
|
82
|
+
in_ruby = sum_series_in_ruby(N)
|
|
83
|
+
|
|
84
|
+
puts "nine series to d = #{N}"
|
|
85
|
+
NAMES.each_with_index do |name, i|
|
|
86
|
+
puts format(" %-18s %19.13f %s Ruby's",
|
|
87
|
+
name, sums[i], sums[i] == in_ruby[i] ? "==" : "!=")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The telescoping sum has an exact answer to be judged against, which is
|
|
91
|
+
# 1 - 1/(n+1).
|
|
92
|
+
telescoped = 1.0 - 1.0 / (N + 1.0)
|
|
93
|
+
puts
|
|
94
|
+
puts format(" the telescoping sum is off by %.1e", (sums[2] - telescoped).abs)
|
|
95
|
+
puts format(" every one of the nine matches the Ruby loop bit for bit: %s",
|
|
96
|
+
sums == in_ruby)
|
|
97
|
+
|
|
98
|
+
# ------------------------------------------------------- where the order is a choice
|
|
99
|
+
|
|
100
|
+
# The harmonic sum again, with the loop written inside the cell instead of
|
|
101
|
+
# being the kernel's own. That is a reduction, and a reduction's accumulator
|
|
102
|
+
# may be split into partial sums: `reassociate: false` asks for the serial
|
|
103
|
+
# order, and the default asks for the faster one, which by splitting the
|
|
104
|
+
# accumulation is also what limits the cancellation.
|
|
105
|
+
def harmonic (n, reassociate)
|
|
106
|
+
out = CScalar.double() { 0.0 }
|
|
107
|
+
CArray.jit_for(1, reassociate: reassociate) { |c|
|
|
108
|
+
total = 0.0
|
|
109
|
+
(1..n).each { |k| total = total + 1.0 / (k * 1.0) }
|
|
110
|
+
out[] = total
|
|
111
|
+
}
|
|
112
|
+
out[0]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
split = harmonic(N, true)
|
|
116
|
+
serial = harmonic(N, false)
|
|
117
|
+
by_carray = (1.0 / CArray.double(N).seq!(1.0)).sum
|
|
118
|
+
|
|
119
|
+
puts
|
|
120
|
+
puts "the harmonic sum as an inner loop"
|
|
121
|
+
puts format(" split into partial sums %.15f", split)
|
|
122
|
+
puts format(" the serial order %.15f", serial)
|
|
123
|
+
puts format(" serial == the outer loop above and Ruby's: %s", serial == sums[5])
|
|
124
|
+
puts format(" split == CArray's own sum: %s", split == by_carray)
|
|
125
|
+
|
|
126
|
+
# And what the pass costs each way.
|
|
127
|
+
sum_series(N) # compiled and cached on the first call
|
|
128
|
+
|
|
129
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
130
|
+
3.times { sum_series(N) }
|
|
131
|
+
compiled = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / 3
|
|
132
|
+
|
|
133
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
134
|
+
sum_series_in_ruby(N)
|
|
135
|
+
interpreted = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
136
|
+
|
|
137
|
+
puts
|
|
138
|
+
puts format("one pass over %d terms", N)
|
|
139
|
+
puts format(" compiled %6.1f ms", compiled * 1e3)
|
|
140
|
+
puts format(" the same loop in Ruby %6.1f ms (%.0fx)",
|
|
141
|
+
interpreted * 1e3, interpreted / compiled)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Rotating a point cloud, then projecting it onto a basis
|
|
2
|
+
#
|
|
3
|
+
# Both are sums over an index that appears twice, which is what
|
|
4
|
+
# `CArray.jit_contract` is: the repeated index is summed, so the notation is the
|
|
5
|
+
# formula.
|
|
6
|
+
#
|
|
7
|
+
# rotated[p,i] = sum_j R[i,j] x[p,j] rotate every point
|
|
8
|
+
# cov[a,b] = sum_p c[p,a] c[p,b] the covariance of the cloud
|
|
9
|
+
# coeff[p,m] = sum_k x[p,k] basis[m,k] project onto a basis
|
|
10
|
+
# recon[p,k] = sum_m coeff[p,m] basis[m,k] and build the points back
|
|
11
|
+
#
|
|
12
|
+
# Written as loops these are three lines each and easy to get subtly wrong: an
|
|
13
|
+
# index in the wrong place transposes the answer rather than failing. Here the
|
|
14
|
+
# parameter list states the axis order, and an index whose axes disagree is
|
|
15
|
+
# refused before anything runs.
|
|
16
|
+
#
|
|
17
|
+
# ruby examples/applications/point_cloud.rb
|
|
18
|
+
|
|
19
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
20
|
+
require "carray/jit"
|
|
21
|
+
|
|
22
|
+
count = 200_000
|
|
23
|
+
random = Random.new(20260902)
|
|
24
|
+
|
|
25
|
+
# A flattened, tilted blob: wide in x, narrower in y, nearly flat in z.
|
|
26
|
+
points = CArray.double(count, 3) { |p, k| random.rand(-1.0..1.0) }
|
|
27
|
+
points[nil, 1] = points[nil, 1] * 0.4
|
|
28
|
+
points[nil, 2] = points[nil, 2] * 0.05
|
|
29
|
+
|
|
30
|
+
angle = Math::PI / 6
|
|
31
|
+
rotation = CArray.double(3, 3)
|
|
32
|
+
rotation[0, nil] = [Math.cos(angle), -Math.sin(angle), 0.0]
|
|
33
|
+
rotation[1, nil] = [Math.sin(angle), Math.cos(angle), 0.0]
|
|
34
|
+
rotation[2, nil] = [0.0, 0.0, 1.0]
|
|
35
|
+
|
|
36
|
+
# j is summed because it appears twice; p and i are free, and they come out as
|
|
37
|
+
# the result's axes in the order the block named them.
|
|
38
|
+
rotated = CArray.jit_contract { |p, i, j| points[p, j] * rotation[i, j] }
|
|
39
|
+
|
|
40
|
+
puts "#{count} points, rotated by #{(angle * 180 / Math::PI).round} degrees"
|
|
41
|
+
puts " spread per axis, before #{points.stddev(axis: 0).to_a.map { |v| v.round(4) }.inspect}"
|
|
42
|
+
puts " after #{rotated.stddev(axis: 0).to_a.map { |v| v.round(4) }.inspect}"
|
|
43
|
+
|
|
44
|
+
# The covariance: p is the index appearing twice, so p is what is summed --
|
|
45
|
+
# the sum over points. The same axis of the same array is read at two
|
|
46
|
+
# independent positions, a and b, which is the whole shape of the thing.
|
|
47
|
+
centred = rotated - rotated.mean(axis: 0).reshape(1, 3)
|
|
48
|
+
covariance = CArray.jit_contract { |a, b, p| centred[p, a] * centred[p, b] } / count
|
|
49
|
+
puts " covariance"
|
|
50
|
+
covariance.to_a.each { |row| puts " " + row.map { |v| format('%9.5f', v) }.join }
|
|
51
|
+
|
|
52
|
+
# Its trace does not change under a rotation, which checks both contractions
|
|
53
|
+
# at once.
|
|
54
|
+
original = points - points.mean(axis: 0).reshape(1, 3)
|
|
55
|
+
before = CArray.jit_contract { |a, b, p| original[p, a] * original[p, b] } / count
|
|
56
|
+
puts format(" trace %.8f before the rotation, %.8f after",
|
|
57
|
+
CArray.jit_contract { |a| before[a, a] }[0],
|
|
58
|
+
CArray.jit_contract { |a| covariance[a, a] }[0])
|
|
59
|
+
|
|
60
|
+
# The plane the blob lies in, in the rotated frame: the two rows of the
|
|
61
|
+
# rotation are an orthonormal basis for it.
|
|
62
|
+
basis = rotation[0..1, nil]
|
|
63
|
+
|
|
64
|
+
coefficients = CArray.jit_contract { |p, m, k| rotated[p, k] * basis[m, k] }
|
|
65
|
+
reconstructed = CArray.jit_contract { |p, k, m| coefficients[p, m] * basis[m, k] }
|
|
66
|
+
|
|
67
|
+
residual = ((rotated - reconstructed) ** 2).sum / count
|
|
68
|
+
puts format(" dropping the third direction costs %.2e per point", residual)
|
|
69
|
+
puts format(" which is the variance that was in it: %.2e",
|
|
70
|
+
rotated[nil, 2].stddev ** 2)
|
|
71
|
+
|
|
72
|
+
# The distance of every point from the origin is *not* a contraction, and this
|
|
73
|
+
# is the place the convention bites: in `x[p,k] * x[p,k]` the index p appears
|
|
74
|
+
# twice as well, so it would be summed too and the answer would be one number.
|
|
75
|
+
# A quantity per point is a per-cell loop, and says so.
|
|
76
|
+
squared = CArray.double(count)
|
|
77
|
+
CArray.jit_for(count) { |p|
|
|
78
|
+
total = 0.0
|
|
79
|
+
(0...3).each { |k| total = total + rotated[p, k] * rotated[p, k] }
|
|
80
|
+
squared[p] = total
|
|
81
|
+
}
|
|
82
|
+
puts format(" furthest point %.4f away", Math.sqrt(squared.max))
|
|
83
|
+
|
|
84
|
+
# The same rotation written as a Ruby loop.
|
|
85
|
+
reference = CArray.double(count, 3)
|
|
86
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
87
|
+
count.times do |p|
|
|
88
|
+
3.times do |i|
|
|
89
|
+
total = 0.0
|
|
90
|
+
3.times { |j| total += points[p, j] * rotation[i, j] }
|
|
91
|
+
reference[p, i] = total
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
interpreted = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
95
|
+
|
|
96
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
97
|
+
CArray.jit_contract { |p, i, j| points[p, j] * rotation[i, j] }
|
|
98
|
+
compiled = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
99
|
+
|
|
100
|
+
puts format(" rotation: %.1f ms compiled, %.0f ms as a Ruby loop (%.0fx)",
|
|
101
|
+
compiled * 1e3, interpreted * 1e3, interpreted / compiled)
|
|
102
|
+
puts " identical: #{rotated.to_a == reference.to_a}"
|
|
103
|
+
|
|
104
|
+
# An index whose axes disagree is the mistake this notation exists to catch.
|
|
105
|
+
begin
|
|
106
|
+
wrong = CArray.double(4, 4).seq!(1.0)
|
|
107
|
+
CArray.jit_contract { |p, i, j| points[p, j] * wrong[i, j] }
|
|
108
|
+
rescue CArray::JIT::Unsupported => error
|
|
109
|
+
puts " refused: #{error.message.lines.first.strip}"
|
|
110
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Quicksort, written in Ruby and compiled to a C function.
|
|
2
|
+
#
|
|
3
|
+
# CArray already sorts, and this is not a better sort -- it is the naive
|
|
4
|
+
# textbook one, last element as the pivot. What it shows is that the body of
|
|
5
|
+
# a C function can be written here: it takes a pointer and two indices, walks
|
|
6
|
+
# the run, swaps through the pointer, and calls itself. The block stays
|
|
7
|
+
# runnable in Ruby, so the same partition can be watched from either side.
|
|
8
|
+
#
|
|
9
|
+
# ruby examples/applications/quicksort.rb
|
|
10
|
+
|
|
11
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
12
|
+
require "carray/jit"
|
|
13
|
+
|
|
14
|
+
# The declaration is C's own, and it is what settles everything: `double v[]`
|
|
15
|
+
# is a run of numbers the function may write, `int64_t` the indices, and the
|
|
16
|
+
# name in the declaration is what puts the function in scope inside its own
|
|
17
|
+
# body -- an anonymous `double (*)(...)` has nothing to call itself by.
|
|
18
|
+
#
|
|
19
|
+
# There is no `void` here because a compiled body has to produce a value, so
|
|
20
|
+
# the return is the C convention for "nothing went wrong".
|
|
21
|
+
quicksort = CArray.jit_function(
|
|
22
|
+
"int quicksort(double v[], int64_t low, int64_t high)"
|
|
23
|
+
) { |v, low, high|
|
|
24
|
+
if low < high
|
|
25
|
+
pivot = v[high] # Lomuto's partition, as written
|
|
26
|
+
smaller = low - 1
|
|
27
|
+
scan = low
|
|
28
|
+
while scan < high
|
|
29
|
+
if v[scan] <= pivot
|
|
30
|
+
smaller = smaller + 1
|
|
31
|
+
held = v[smaller]
|
|
32
|
+
v[smaller] = v[scan]
|
|
33
|
+
v[scan] = held
|
|
34
|
+
end
|
|
35
|
+
scan = scan + 1
|
|
36
|
+
end
|
|
37
|
+
held = v[smaller+1]
|
|
38
|
+
v[smaller+1] = v[high]
|
|
39
|
+
v[high] = held
|
|
40
|
+
# Called for what they do to the run; the 0 each answers goes nowhere,
|
|
41
|
+
# which is what a call standing where a statement stands means here and
|
|
42
|
+
# in C.
|
|
43
|
+
quicksort.call(v, low, smaller)
|
|
44
|
+
quicksort.call(v, smaller + 2, high)
|
|
45
|
+
end
|
|
46
|
+
0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
n = 200_000
|
|
50
|
+
random = Random.new(20260905)
|
|
51
|
+
values = CArray.double(n) { |i| random.rand }
|
|
52
|
+
|
|
53
|
+
sorted = values.copy # `to_ca` would answer this array
|
|
54
|
+
quicksort.call(sorted, 0, n - 1)
|
|
55
|
+
|
|
56
|
+
puts "quicksort"
|
|
57
|
+
puts " sorted #{(0...n-1).all? { |i| sorted[i] <= sorted[i+1] }}"
|
|
58
|
+
puts " same cells as CArray #{sorted.to_a == values.sort.to_a}"
|
|
59
|
+
|
|
60
|
+
# The block is still there, and still Ruby. Run that way the partition is
|
|
61
|
+
# interpreted and the two halves go back through the compiled function, which
|
|
62
|
+
# is the same body reached by the other road.
|
|
63
|
+
in_ruby = values[0...200].copy
|
|
64
|
+
quicksort.block.call(in_ruby, 0, 199)
|
|
65
|
+
puts " the block agrees #{in_ruby.to_a == values[0...200].sort.to_a}"
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------- what it costs
|
|
68
|
+
|
|
69
|
+
def timed (repeats)
|
|
70
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
71
|
+
repeats.times { yield }
|
|
72
|
+
(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / repeats
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
scratch = values.copy
|
|
76
|
+
compiled = timed(5) { scratch[nil] = values; quicksort.call(scratch, 0, n - 1) }
|
|
77
|
+
carray = timed(5) { values.sort }
|
|
78
|
+
ruby_array = timed(5) { values.to_a.sort }
|
|
79
|
+
|
|
80
|
+
def ruby_quicksort (v, low, high)
|
|
81
|
+
return if low >= high
|
|
82
|
+
pivot = v[high]
|
|
83
|
+
smaller = low - 1
|
|
84
|
+
(low...high).each do |scan|
|
|
85
|
+
if v[scan] <= pivot
|
|
86
|
+
smaller += 1
|
|
87
|
+
v[smaller], v[scan] = v[scan], v[smaller]
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
v[smaller+1], v[high] = v[high], v[smaller+1]
|
|
91
|
+
ruby_quicksort(v, low, smaller)
|
|
92
|
+
ruby_quicksort(v, smaller + 2, high)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
interpreted_source = values.to_a
|
|
96
|
+
interpreted = timed(1) { ruby_quicksort(interpreted_source.dup, 0, n - 1) }
|
|
97
|
+
|
|
98
|
+
puts
|
|
99
|
+
puts format("%d doubles", n)
|
|
100
|
+
puts format(" this quicksort, compiled %6.1f ms", compiled * 1e3)
|
|
101
|
+
puts format(" CArray#sort %6.1f ms", carray * 1e3)
|
|
102
|
+
puts format(" Array#sort %6.1f ms", ruby_array * 1e3)
|
|
103
|
+
puts format(" the same partition in Ruby %6.1f ms (%.0fx)",
|
|
104
|
+
interpreted * 1e3, interpreted / compiled)
|
|
105
|
+
|
|
106
|
+
# The textbook partition lands beside CArray#sort, which is the honest
|
|
107
|
+
# reading of it: both are C walking the same memory, and this one is measured
|
|
108
|
+
# with the copy it needs included. Beating it was never the point -- CArray
|
|
109
|
+
# sorts already. What the compiled body is worth is the case where no such
|
|
110
|
+
# method exists: an order nobody wrote a sort for, a key computed as you go,
|
|
111
|
+
# a run inside a structure CArray has no name for.
|
|
112
|
+
#
|
|
113
|
+
# Two things this naive version keeps that a library sort does not: the pivot
|
|
114
|
+
# is the last element, so an already-sorted run partitions n times and the
|
|
115
|
+
# recursion is n deep, and a compiled function that recurses too deep is a
|
|
116
|
+
# SIGSEGV rather than a SystemStackError. That is C's bargain, taken here
|
|
117
|
+
# along with the pointer. A median-of-three pivot is three more lines, and
|
|
118
|
+
# the reason to write them.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# The classic recursive benchmarks, as compiled C functions.
|
|
2
|
+
#
|
|
3
|
+
# fib, tak, tarai and ackermann are what an interpreter is measured on, and
|
|
4
|
+
# none of them is an array computation: no cell, no extent, nothing to be
|
|
5
|
+
# element-wise about. What they are is a scalar function calling itself,
|
|
6
|
+
# which is `jit_function`'s -- a body written in Ruby, compiled to C, and
|
|
7
|
+
# callable from Ruby or from a kernel.
|
|
8
|
+
#
|
|
9
|
+
# A declaration that gives a *name* puts that name in scope inside its own
|
|
10
|
+
# body, as C does, so the function can recurse. The spelling is `.call`, the
|
|
11
|
+
# one every C function takes here, and that is what keeps the block runnable:
|
|
12
|
+
# the same block is the reference the compiled function is checked against.
|
|
13
|
+
#
|
|
14
|
+
# The parameters are the ones these benchmarks are usually quoted at, so the
|
|
15
|
+
# ratios below can be read beside anyone else's.
|
|
16
|
+
#
|
|
17
|
+
# ruby examples/applications/recursion.rb
|
|
18
|
+
|
|
19
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
20
|
+
require "carray/jit"
|
|
21
|
+
|
|
22
|
+
fib = CArray.jit_function("int64_t fib(int64_t n)") { |n|
|
|
23
|
+
n < 2 ? n : fib.call(n - 1) + fib.call(n - 2)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
tak = CArray.jit_function("int64_t tak(int64_t x, int64_t y, int64_t z)") { |x, y, z|
|
|
27
|
+
y < x ? tak.call(tak.call(x - 1, y, z),
|
|
28
|
+
tak.call(y - 1, z, x),
|
|
29
|
+
tak.call(z - 1, x, y)) : z
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
tarai = CArray.jit_function("int64_t tarai(int64_t x, int64_t y, int64_t z)") { |x, y, z|
|
|
33
|
+
x <= y ? y : tarai.call(tarai.call(x - 1, y, z),
|
|
34
|
+
tarai.call(y - 1, z, x),
|
|
35
|
+
tarai.call(z - 1, x, y))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ack = CArray.jit_function("int64_t ack(int64_t m, int64_t n)") { |m, n|
|
|
39
|
+
m == 0 ? n + 1 : (n == 0 ? ack.call(m - 1, 1) : ack.call(m - 1, ack.call(m, n - 1)))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# The same four in Ruby, which is both the answer to check against and the
|
|
43
|
+
# time to measure against.
|
|
44
|
+
def fib_rb (n) = n < 2 ? n : fib_rb(n - 1) + fib_rb(n - 2)
|
|
45
|
+
def tak_rb (x, y, z) = y < x ? tak_rb(tak_rb(x - 1, y, z), tak_rb(y - 1, z, x), tak_rb(z - 1, x, y)) : z
|
|
46
|
+
def tarai_rb (x, y, z) = x <= y ? y : tarai_rb(tarai_rb(x - 1, y, z), tarai_rb(y - 1, z, x), tarai_rb(z - 1, x, y))
|
|
47
|
+
def ack_rb (m, n) = m == 0 ? n + 1 : (n == 0 ? ack_rb(m - 1, 1) : ack_rb(m - 1, ack_rb(m, n - 1)))
|
|
48
|
+
|
|
49
|
+
def timed
|
|
50
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
51
|
+
value = yield
|
|
52
|
+
[value, Process.clock_gettime(Process::CLOCK_MONOTONIC) - started]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
CASES = [
|
|
56
|
+
["fib(34)", -> (f) { f.call(34) }, -> { fib_rb(34) }],
|
|
57
|
+
["tak(18, 9, 0)", -> (f) { f.call(18, 9, 0) }, -> { tak_rb(18, 9, 0) }],
|
|
58
|
+
["tarai(12, 6, 0)", -> (f) { f.call(12, 6, 0) }, -> { tarai_rb(12, 6, 0) }],
|
|
59
|
+
["ack(3, 9)", -> (f) { f.call(3, 9) }, -> { ack_rb(3, 9) }],
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
puts "a recursion compiled, and the same one interpreted"
|
|
63
|
+
[fib, tak, tarai, ack].zip(CASES) do |function, (name, compiled_call, ruby_call)|
|
|
64
|
+
compiled_call.call(function) # compiled on the way in
|
|
65
|
+
answer, took = timed { compiled_call.call(function) }
|
|
66
|
+
in_ruby, ruby_took = timed { ruby_call.call }
|
|
67
|
+
puts format(" %-16s %8d %7.1f ms compiled %7.1f ms in Ruby (%4.0fx) %s",
|
|
68
|
+
name, answer, took * 1e3, ruby_took * 1e3, ruby_took / took,
|
|
69
|
+
answer == in_ruby ? "agree" : "DIFFER")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# What the block is for
|
|
73
|
+
# ---------------------
|
|
74
|
+
# The function keeps the block it was written from, so the compiled recursion
|
|
75
|
+
# and the Ruby one are the same text and can be put side by side. That is
|
|
76
|
+
# what makes "it means what Ruby means by it" checkable rather than argued.
|
|
77
|
+
puts
|
|
78
|
+
puts " the block is still there: fib.block.call(20) = #{fib.block.call(20)}, " \
|
|
79
|
+
"fib.call(20) = #{fib.call(20)}"
|
|
80
|
+
|
|
81
|
+
# Where a kernel comes in
|
|
82
|
+
# -----------------------
|
|
83
|
+
# A compiled function is not only faster to run, it is reachable per cell: the
|
|
84
|
+
# kernel calls the address directly, rather than crossing back into Ruby -- or
|
|
85
|
+
# into Fiddle, which costs a few hundred nanoseconds a cell, more than most of
|
|
86
|
+
# what it would be called for.
|
|
87
|
+
inputs = CArray.int64(24).seq!(1)
|
|
88
|
+
outputs = CArray.int64(24)
|
|
89
|
+
|
|
90
|
+
CArray.jit_for(24) { |i| outputs[i] = fib.call(inputs[i]) }
|
|
91
|
+
|
|
92
|
+
puts
|
|
93
|
+
puts "fib over an array, one call per cell"
|
|
94
|
+
puts " #{outputs[0..11].to_a.inspect}"
|
|
95
|
+
puts " matches Ruby #{outputs.to_a == inputs.to_a.map { |n| fib_rb(n) }}"
|
|
96
|
+
|
|
97
|
+
_, per_cell = timed { CArray.jit_for(24) { |i| outputs[i] = fib.call(inputs[i]) } }
|
|
98
|
+
_, in_ruby = timed { inputs.to_a.map { |n| fib_rb(n) } }
|
|
99
|
+
puts format(" the pass: %.1f ms compiled, %.1f ms in Ruby (%.0fx)",
|
|
100
|
+
per_cell * 1e3, in_ruby * 1e3, in_ruby / per_cell)
|
|
101
|
+
|
|
102
|
+
# What is not on offer
|
|
103
|
+
# --------------------
|
|
104
|
+
# An anonymous declaration -- the function-pointer type -- has nothing to call
|
|
105
|
+
# itself by, which is C's position too: it is the name in the declaration that
|
|
106
|
+
# is put in scope inside the body, and `int64_t (*)(int64_t)` has none. The
|
|
107
|
+
# `f` below is then an ordinary Ruby local seen from inside, which is a
|
|
108
|
+
# capture, and a capture is what a compiled function does not get.
|
|
109
|
+
begin
|
|
110
|
+
f = CArray.jit_function("int64_t (*)(int64_t)") { |n|
|
|
111
|
+
n < 2 ? n : f.call(n - 1) + f.call(n - 2)
|
|
112
|
+
}
|
|
113
|
+
rescue CArray::JIT::Unsupported => error
|
|
114
|
+
puts
|
|
115
|
+
puts "an anonymous declaration"
|
|
116
|
+
puts " #{error.message.sub(/ \(at line.*/m, "")}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# And nothing here stops a recursion running out of stack: a compiled function
|
|
120
|
+
# that goes too deep is a SIGSEGV, not a SystemStackError. That is C's
|
|
121
|
+
# bargain, taken knowingly -- the same one the `while` loop in a kernel takes.
|