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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +10 -0
  3. data/CHANGELOG.md +84 -0
  4. data/LICENSE +21 -0
  5. data/README.md +88 -0
  6. data/bin/carray-jit +194 -0
  7. data/carray-jit.gemspec +41 -0
  8. data/docs/00_Introduction.md +40 -0
  9. data/docs/01_GettingStarted.md +80 -0
  10. data/docs/02_KernelShapes.md +397 -0
  11. data/docs/03_SupportedFeatures.md +595 -0
  12. data/docs/04_Compiling.md +234 -0
  13. data/docs/05_DesignNotes.md +136 -0
  14. data/docs/06_Cheatsheet.md +177 -0
  15. data/examples/README.md +56 -0
  16. data/examples/applications/game_of_life.rb +161 -0
  17. data/examples/applications/heat_equation.rb +117 -0
  18. data/examples/applications/kepler.rb +178 -0
  19. data/examples/applications/mandelbrot.rb +151 -0
  20. data/examples/applications/moving_average.rb +124 -0
  21. data/examples/applications/partial_sums.rb +141 -0
  22. data/examples/applications/point_cloud.rb +110 -0
  23. data/examples/applications/quicksort.rb +118 -0
  24. data/examples/applications/recursion.rb +121 -0
  25. data/examples/applications/relaxation.rb +115 -0
  26. data/examples/applications/sensor_gaps.rb +118 -0
  27. data/examples/applications/sieve.rb +95 -0
  28. data/examples/applications/sobel_edges.rb +80 -0
  29. data/examples/features/01_element_wise.rb +69 -0
  30. data/examples/features/02_stencil.rb +40 -0
  31. data/examples/features/03_recurrence.rb +50 -0
  32. data/examples/features/04_thomas.rb +81 -0
  33. data/examples/features/05_reduction.rb +90 -0
  34. data/examples/features/06_jit_contract.rb +58 -0
  35. data/examples/features/07_masks.rb +55 -0
  36. data/examples/features/08_views.rb +46 -0
  37. data/examples/features/09_inspecting.rb +55 -0
  38. data/examples/features/10_complex.rb +107 -0
  39. data/examples/features/11_c_functions.rb +260 -0
  40. data/examples/features/12_sweep.rb +139 -0
  41. data/examples/features/13_cscalar.rb +80 -0
  42. data/examples/features/14_stencil_window.rb +106 -0
  43. data/examples/features/15_loops.rb +148 -0
  44. data/examples/features/16_raising.rb +69 -0
  45. data/ext/carray_jit_access/carray_jit_access.c +460 -0
  46. data/ext/carray_jit_access/extconf.rb +8 -0
  47. data/lib/carray/jit/analyzer.rb +1847 -0
  48. data/lib/carray/jit/block_reader.rb +139 -0
  49. data/lib/carray/jit/c_function.rb +777 -0
  50. data/lib/carray/jit/c_generator.rb +2305 -0
  51. data/lib/carray/jit/compiler.rb +468 -0
  52. data/lib/carray/jit/errors.rb +37 -0
  53. data/lib/carray/jit/expression.rb +202 -0
  54. data/lib/carray/jit/kernel.rb +509 -0
  55. data/lib/carray/jit/node.rb +573 -0
  56. data/lib/carray/jit/sweep.rb +97 -0
  57. data/lib/carray/jit/type_assignment.rb +811 -0
  58. data/lib/carray/jit/version.rb +5 -0
  59. data/lib/carray/jit.rb +1210 -0
  60. 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.