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,161 @@
1
+ # Conway's Game of Life
2
+ #
3
+ # A generation is a stencil: nine reads and two comparisons per cell. Written
4
+ # with CArray's operators it would be eight shifted arrays added together, and
5
+ # eight passes over the board to do it; written as a Ruby loop it would be a
6
+ # block call per cell. Written here it is the rule as it is stated, run once
7
+ # per cell.
8
+ #
9
+ # It is written twice. With the indices named the extents say which cells are
10
+ # written, and the border is what they leave out -- the board has edges, and a
11
+ # glider that reaches one dies there. With windows the border is an argument
12
+ # instead, and `border: :wrap` is what makes the board the torus the rules are
13
+ # usually stated on.
14
+ #
15
+ # ruby examples/applications/game_of_life.rb
16
+
17
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
18
+ require "carray/jit"
19
+
20
+ ROWS, COLUMNS = 24, 48
21
+
22
+ def step (current, following)
23
+ rows, columns = current.dim
24
+ CArray.jit_for(1...(rows-1), 1...(columns-1)) { |i, j|
25
+ neighbours = current[i-1, j-1] + current[i-1, j] + current[i-1, j+1] +
26
+ current[i, j-1] + current[i, j+1] +
27
+ current[i+1, j-1] + current[i+1, j] + current[i+1, j+1]
28
+ if neighbours == 3
29
+ following[i, j] = 1
30
+ elsif neighbours == 2
31
+ following[i, j] = current[i, j]
32
+ else
33
+ following[i, j] = 0
34
+ end
35
+ }
36
+ end
37
+
38
+ # The same rule with windows onto the board instead of indices. `a[0, 0]` is
39
+ # the cell, `a[-1, -1]` its neighbour, and the block's value is what the cell
40
+ # gets -- so the rule is an expression, and there is no loop and no extent to
41
+ # write. `border: :wrap` says that a read off one edge comes back the other
42
+ # side, which is the whole of what makes this a torus.
43
+ def step_torus (current, following)
44
+ CArray.jit_stencil(current, border: :wrap, into: following) { |a|
45
+ neighbours = a[-1, -1] + a[-1, 0] + a[-1, 1] +
46
+ a[0, -1] + a[0, 1] +
47
+ a[1, -1] + a[1, 0] + a[1, 1]
48
+ neighbours == 3 ? 1 : (neighbours == 2 ? a[0, 0] : 0)
49
+ }
50
+ end
51
+
52
+ def draw (board, title)
53
+ puts title
54
+ board.to_a.each { |row| puts " " + row.map { |cell| cell == 1 ? "#" : "·" }.join }
55
+ end
56
+
57
+ board = CArray.int32(ROWS, COLUMNS)
58
+ scratch = CArray.int32(ROWS, COLUMNS)
59
+
60
+ # A glider, and an r-pentomino to make a mess of things.
61
+ [[2,2],[3,3],[4,1],[4,2],[4,3]].each { |i, j| board[i, j] = 1 }
62
+ [[12,20],[12,21],[13,19],[13,20],[14,20]].each { |i, j| board[i, j] = 1 }
63
+
64
+ draw(board, "generation 0")
65
+
66
+ 64.times do |generation|
67
+ step(board, scratch)
68
+ board, scratch = scratch, board
69
+ draw(board, "generation #{generation + 1}") if generation + 1 == 16 ||
70
+ generation + 1 == 32
71
+ end
72
+
73
+ # The glider has walked down and to the right; the r-pentomino has burned
74
+ # itself out against the edge. The board is a torus in the usual statement of
75
+ # the rules -- here the border is simply not written, which is what the extents
76
+ # say, and nothing else in the program had to know it.
77
+
78
+ # Away from the edges the two spellings are the same rule, and agree on it.
79
+ sample = CArray.int32(ROWS, COLUMNS)
80
+ [[5,5],[5,6],[5,7],[6,4],[6,5],[6,6],[9,20],[10,21],[11,19],[11,20],[11,21]].each { |i, j|
81
+ sample[i, j] = 1
82
+ }
83
+ by_index = CArray.int32(ROWS, COLUMNS)
84
+ by_window = CArray.int32(ROWS, COLUMNS)
85
+ step(sample, by_index)
86
+ step_torus(sample, by_window)
87
+ interior = [1...(ROWS-1), 1...(COLUMNS-1)]
88
+ puts
89
+ puts "the two spellings, on the interior: " \
90
+ "#{by_index[*interior].to_a == by_window[*interior].to_a}"
91
+
92
+ # On the torus the glider does not run out of board. A small one, so that it
93
+ # reaches the corner while there is still something to watch.
94
+ SIDE = 16
95
+ torus = CArray.int32(SIDE, SIDE)
96
+ spare = CArray.int32(SIDE, SIDE)
97
+ [[1,2],[2,3],[3,1],[3,2],[3,3]].each { |i, j| torus[i, j] = 1 }
98
+ started_from = torus.copy # `to_ca` would answer this same array
99
+
100
+ draw(torus, "torus, generation 0")
101
+ 64.times do |generation|
102
+ step_torus(torus, spare)
103
+ torus, spare = spare, torus
104
+ draw(torus, "torus, generation #{generation + 1}") if generation + 1 == 52 ||
105
+ generation + 1 == 64
106
+ end
107
+
108
+ # It left by the bottom right and came back at the top left, unchanged: a
109
+ # glider on a torus of side 16 is back where it started after 64 generations,
110
+ # which is the check on `:wrap` -- the board has no edge for it to die on.
111
+ puts " the glider came home: #{torus.to_a == started_from.to_a}"
112
+
113
+ # How long a generation takes, against the same rule as a Ruby loop.
114
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
115
+ 100.times { step(board, scratch); board, scratch = scratch, board }
116
+ compiled = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / 100
117
+
118
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
119
+ 5.times do
120
+ (1...(ROWS-1)).each do |i|
121
+ (1...(COLUMNS-1)).each do |j|
122
+ neighbours = board[i-1, j-1] + board[i-1, j] + board[i-1, j+1] +
123
+ board[i, j-1] + board[i, j+1] +
124
+ board[i+1, j-1] + board[i+1, j] + board[i+1, j+1]
125
+ scratch[i, j] = neighbours == 3 ? 1 : (neighbours == 2 ? board[i, j] : 0)
126
+ end
127
+ end
128
+ end
129
+ interpreted = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / 5
130
+
131
+ step_torus(board, scratch) # compiled once, then measured
132
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
133
+ 100.times { step_torus(board, scratch); board, scratch = scratch, board }
134
+ wrapped = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / 100
135
+
136
+ puts format("a generation: %.3f ms with the indices named, %.3f ms as a window " \
137
+ "on a torus, %.3f ms in Ruby (%.0fx)",
138
+ compiled * 1e3, wrapped * 1e3, interpreted * 1e3, interpreted / compiled)
139
+
140
+ # The window is not the slower rule; this board is small. Its frame is a
141
+ # tenth of its cells, and the frame is where the wrap is woven into the reads
142
+ # -- and a generation here is measured in tens of microseconds, so the cost of
143
+ # making the call at all is in the number too. On a board where neither is
144
+ # true the two are level:
145
+ LARGE = 1200
146
+ crowd = CArray.int32(LARGE, LARGE).random!(2)
147
+ next_crowd = CArray.int32(LARGE, LARGE)
148
+ step(crowd, next_crowd)
149
+ step_torus(crowd, next_crowd)
150
+
151
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
152
+ 10.times { step(crowd, next_crowd) }
153
+ large_indexed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / 10
154
+
155
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
156
+ 10.times { step_torus(crowd, next_crowd) }
157
+ large_wrapped = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / 10
158
+
159
+ puts format("a generation on %d x %d: %.2f ms with the indices named, " \
160
+ "%.2f ms as a window on a torus",
161
+ LARGE, LARGE, large_indexed * 1e3, large_wrapped * 1e3)
@@ -0,0 +1,117 @@
1
+ # Heat diffusion in a rod, solved implicitly
2
+ #
3
+ # The explicit scheme is a stencil and is easy; it is also unstable unless the
4
+ # time step is tiny. The implicit one is stable at any step, at the price of
5
+ # solving a tridiagonal system every step -- and that solver is two sequential
6
+ # sweeps, which is exactly what an array library cannot vectorise for you.
7
+ #
8
+ # So this is the shape a lot of numerical code has: an outer loop in Ruby, and
9
+ # per step a couple of kernels that do the work.
10
+ #
11
+ # ruby examples/applications/heat_equation.rb
12
+
13
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
14
+ require "carray/jit"
15
+
16
+ n = 400 # points along the rod
17
+ steps = 2_000
18
+ dx = 1.0 / (n - 1)
19
+ dt = 2.5e-5 # eight times the step an explicit scheme would survive
20
+ r = dt / (dx * dx)
21
+
22
+ temperature = CArray.double(n)
23
+ # A hot band in the middle of a cold rod, ends held at zero.
24
+ CArray.jit_for(n) { |i| temperature[i] = 0.0 }
25
+ (n * 4 / 10...(n * 6 / 10)).each { |i| temperature[i] = 1.0 }
26
+
27
+ initial_heat = temperature.sum * dx
28
+
29
+ # Backward Euler: (1 + 2r) T[i] - r T[i-1] - r T[i+1] = T_old[i], which is
30
+ # tridiagonal with constant coefficients.
31
+ lower = -r
32
+ diagonal = 1.0 + 2.0 * r
33
+ upper = -r
34
+
35
+ cc = CArray.double(n)
36
+ dd = CArray.double(n)
37
+
38
+ def solve (temperature, cc, dd, lower, diagonal, upper, n)
39
+ cc[0] = 0.0 # boundary: T[0] fixed
40
+ dd[0] = temperature[0]
41
+
42
+ CArray.jit_for(1...n) { |i| # forward sweep
43
+ denominator = diagonal - lower * cc[i-1]
44
+ cc[i] = upper / denominator
45
+ dd[i] = (temperature[i] - lower * dd[i-1]) / denominator
46
+ }
47
+
48
+ # boundary: T[n-1] fixed
49
+ dd[n-1] = temperature[n-1]
50
+ temperature[n-1] = dd[n-1]
51
+
52
+ CArray.jit_for((n-2).step(0, -1)) { |i| # back substitution
53
+ temperature[i] = dd[i] - cc[i] * temperature[i+1]
54
+ }
55
+ end
56
+
57
+ def draw (temperature, label)
58
+ n = temperature.dim[0]
59
+ row = (0...72).map { |k|
60
+ value = temperature[k * (n - 1) / 71]
61
+ " .:-=+*#@"[[(value * 8).round, 8].min] || " "
62
+ }
63
+ puts " #{label.rjust(6)} |#{row.join}|"
64
+ end
65
+
66
+ puts "rod of #{n} points, #{steps} implicit steps of dt = #{format('%.1e', dt)}"
67
+ puts format(" an explicit scheme would need dt < %.1e here", dx * dx / 2)
68
+ draw(temperature, "0")
69
+
70
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
71
+ steps.times do |step|
72
+ solve(temperature, cc, dd, lower, diagonal, upper, n)
73
+ draw(temperature, (step + 1).to_s) if [50, 200, 800, 2000].include?(step + 1)
74
+ end
75
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
76
+
77
+ puts format(" %.0f ms for %d steps -- %.1f us per solve of %d unknowns",
78
+ elapsed * 1e3, steps, elapsed / steps * 1e6, n)
79
+
80
+ # The same solve written as Ruby loops, for comparison. At n = 400 a fair
81
+ # part of the compiled time is the two kernel calls rather than the arithmetic
82
+ # in them, so this is the regime where the gap is smallest -- and it is still
83
+ # worth having.
84
+ def ruby_solve (temperature, cc, dd, lower, diagonal, upper, n)
85
+ cc[0] = 0.0
86
+ dd[0] = temperature[0]
87
+ (1...n).each do |i|
88
+ denominator = diagonal - lower * cc[i-1]
89
+ cc[i] = upper / denominator
90
+ dd[i] = (temperature[i] - lower * dd[i-1]) / denominator
91
+ end
92
+ dd[n-1] = temperature[n-1]
93
+ temperature[n-1] = dd[n-1]
94
+ (n-2).step(0, -1) do |i|
95
+ temperature[i] = dd[i] - cc[i] * temperature[i+1]
96
+ end
97
+ end
98
+
99
+ def time (repeats)
100
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
101
+ repeats.times { yield }
102
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / repeats
103
+ end
104
+
105
+ [400, 4_000, 40_000].each do |size|
106
+ field = CArray.double(size).seq! { |i| i.to_f / size }
107
+ work = [CArray.double(size), CArray.double(size)]
108
+ compiled = time(20) { solve(field, *work, lower, diagonal, upper, size) }
109
+ interpreted = time(3) { ruby_solve(field, *work, lower, diagonal, upper, size) }
110
+ puts format(" n = %6d %7.1f us compiled %8.1f us Ruby %3.0fx",
111
+ size, compiled * 1e6, interpreted * 1e6, interpreted / compiled)
112
+ end
113
+
114
+ # Heat leaves through the ends, so the total falls; what it must not do is
115
+ # oscillate or blow up, which is the whole reason for solving implicitly.
116
+ puts format(" heat: %.4f initially, %.4f now, peak temperature %.4f",
117
+ initial_heat, temperature.sum * dx, temperature.max)
@@ -0,0 +1,178 @@
1
+ # Kepler's equation, by Newton's method.
2
+ #
3
+ # M = E - e sin E
4
+ #
5
+ # Given the mean anomaly M of a body on an ellipse, the eccentric anomaly E is
6
+ # what the equation has to be solved for, and there is no closed form. Newton
7
+ # converges in a handful of passes -- but not the same handful for every cell:
8
+ # near e = 1 and M = 0 the correction is small and the iteration crawls, while
9
+ # a nearly circular orbit is done in two.
10
+ #
11
+ # So the number of passes is a property of the cell, not of the program, and
12
+ # that is what `while` is for. A cap in an extent would be the wrong shape
13
+ # here: it would have to be the worst cell's count, and every other cell would
14
+ # be written to iterate as long as the worst one.
15
+ #
16
+ # ruby examples/applications/kepler.rb
17
+
18
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
19
+ require "carray/jit"
20
+
21
+ n = 200_000
22
+ tolerance = 1e-13
23
+
24
+ mean_anomaly = CArray.double(n) { |i| 2.0 * Math::PI * i / n - Math::PI }
25
+
26
+ def solve (mean_anomaly, eccentricity, tolerance)
27
+ eccentric = CArray.double(mean_anomaly.elements)
28
+ CArray.jit_for(mean_anomaly.elements) { |i|
29
+ guess = mean_anomaly[i] # good enough for any e < 1
30
+ step = 1.0
31
+ # The condition is read at the top of every pass, as Ruby's is, so `step`
32
+ # has to be a local before the loop -- a name the body alone assigned
33
+ # would not be in scope where the condition wants it.
34
+ while step.abs > tolerance
35
+ step = (guess - eccentricity * Math.sin(guess) - mean_anomaly[i]) /
36
+ (1.0 - eccentricity * Math.cos(guess))
37
+ guess = guess - step
38
+ end
39
+ eccentric[i] = guess
40
+ }
41
+ eccentric
42
+ end
43
+
44
+ puts "Kepler's equation over #{n} anomalies"
45
+ [0.0167, 0.2056, 0.6, 0.9].each do |eccentricity|
46
+ eccentric = solve(mean_anomaly, eccentricity, tolerance)
47
+ # The check is the equation itself: put E back and see whether M comes out.
48
+ residual = (eccentric - eccentricity * eccentric.sin - mean_anomaly).abs.max
49
+ puts format(" e = %.4f largest residual %.2e", eccentricity, residual)
50
+ end
51
+
52
+ # How many passes each cell took, which is the thing the extent could not have
53
+ # known. Counting is a second local; the loop is otherwise the same.
54
+ eccentricity = 0.9
55
+ passes = CArray.int32(n)
56
+ roots = CArray.double(n)
57
+ CArray.jit_for(n) { |i|
58
+ guess = mean_anomaly[i]
59
+ step = 1.0
60
+ taken = 0
61
+ while step.abs > tolerance
62
+ step = (guess - eccentricity * Math.sin(guess) - mean_anomaly[i]) /
63
+ (1.0 - eccentricity * Math.cos(guess))
64
+ guess = guess - step
65
+ taken = taken + 1
66
+ end
67
+ roots[i] = guess
68
+ passes[i] = taken
69
+ }
70
+
71
+ puts
72
+ puts "at e = 0.9"
73
+ puts " passes #{passes.min}..#{passes.max}, #{format('%.2f', passes.sum.to_f / n)} on average"
74
+ puts " the slowest cell is M = #{format('%.4f', mean_anomaly[passes.max_addr])}"
75
+ puts " a cap in an extent would have been #{passes.max} for every cell"
76
+
77
+ # The same loop written in Ruby. A `while` in the kernel is Ruby's `while`:
78
+ # the condition is read at the top of every pass, `step` is a local before the
79
+ # loop, and no accumulation is reassociated -- so the two take the same passes
80
+ # over the same arithmetic.
81
+ in_ruby = CArray.double(n)
82
+ (0...n).each do |i|
83
+ guess = mean_anomaly[i]
84
+ step = 1.0
85
+ while step.abs > tolerance
86
+ step = (guess - eccentricity * Math.sin(guess) - mean_anomaly[i]) /
87
+ (1.0 - eccentricity * Math.cos(guess))
88
+ guess = guess - step
89
+ end
90
+ in_ruby[i] = guess
91
+ end
92
+
93
+ # What they do not share is the sine. A few hundred cells come out a bit
94
+ # apart, and it is worth finding where that comes from before blaming the
95
+ # loop, because it is not the loop: the kernel asks for the sine and the
96
+ # cosine of the same argument in the same pass, and the C compiler answers
97
+ # both with one call to the library's `sincos`, which rounds a few arguments
98
+ # differently from `sin` on its own. Ruby calls `sin`.
99
+ differing = (0...n).count { |i| roots[i] != in_ruby[i] }
100
+
101
+ iterate = CArray.double(n) # one Newton step, in Ruby
102
+ (0...n).each do |i|
103
+ guess = mean_anomaly[i]
104
+ iterate[i] = guess - (guess - eccentricity * Math.sin(guess) - mean_anomaly[i]) /
105
+ (1.0 - eccentricity * Math.cos(guess))
106
+ end
107
+
108
+ alone = CArray.double(n)
109
+ CArray.jit_for(n) { |i| alone[i] = Math.sin(iterate[i]) }
110
+ together = CArray.double(n)
111
+ unused = CArray.double(n)
112
+ CArray.jit_for(n) { |i|
113
+ together[i] = Math.sin(iterate[i])
114
+ unused[i] = Math.cos(iterate[i])
115
+ }
116
+
117
+ puts format(" differs from Ruby %d of %d cells, largest %.1e",
118
+ differing, n, (roots - in_ruby).abs.max)
119
+ puts format(" a sine on its own differs from Ruby's for %d arguments",
120
+ (0...n).count { |i| alone[i] != Math.sin(iterate[i]) })
121
+ puts format(" a sine beside a cosine differs for %d of the same arguments",
122
+ (0...n).count { |i| together[i] != Math.sin(iterate[i]) })
123
+
124
+ # With no transcendental in it, the same Newton iteration agrees bit for bit
125
+ # -- a cube root, written the same way, over the same anomalies:
126
+ cube = CArray.double(n)
127
+ CArray.jit_for(n) { |i|
128
+ x = 1.0 + mean_anomaly[i].abs
129
+ step = 1.0
130
+ while step.abs > 1e-14 * x
131
+ step = (x * x * x - (1.0 + mean_anomaly[i].abs)) / (3.0 * x * x)
132
+ x = x - step
133
+ end
134
+ cube[i] = x
135
+ }
136
+ cube_in_ruby = CArray.double(n)
137
+ (0...n).each do |i|
138
+ a = 1.0 + mean_anomaly[i].abs
139
+ x = a
140
+ step = 1.0
141
+ while step.abs > 1e-14 * x
142
+ step = (x * x * x - a) / (3.0 * x * x)
143
+ x = x - step
144
+ end
145
+ cube_in_ruby[i] = x
146
+ end
147
+ puts " a cube root by the same loop, bit for bit: #{cube.to_a == cube_in_ruby.to_a}"
148
+
149
+ # What it cost each way. The kernel is compiled on its first call and cached,
150
+ # so the one above is what paid for it.
151
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
152
+ 5.times { solve(mean_anomaly, eccentricity, tolerance) }
153
+ compiled = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / 5
154
+
155
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
156
+ (0...n).each do |i|
157
+ guess = mean_anomaly[i]
158
+ step = 1.0
159
+ while step.abs > tolerance
160
+ step = (guess - eccentricity * Math.sin(guess) - mean_anomaly[i]) /
161
+ (1.0 - eccentricity * Math.cos(guess))
162
+ guess = guess - step
163
+ end
164
+ in_ruby[i] = guess
165
+ end
166
+ interpreted = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
167
+
168
+ puts
169
+ puts format(" %.2f ms compiled, %.2f ms in Ruby (%.0fx)",
170
+ compiled * 1e3, interpreted * 1e3, interpreted / compiled)
171
+
172
+ # What `while` gives up is the guarantee that the loop ends. Newton on this
173
+ # equation converges for every e < 1 from this starting guess, which is why it
174
+ # is written this way here; a solver for an equation with no such argument
175
+ # behind it wants the bounded spelling, where the extent holds the cap and a
176
+ # cell that ran out can be told from one that converged. A kernel that does
177
+ # not return cannot be interrupted -- Ctrl-C is not delivered until the call
178
+ # comes back.
@@ -0,0 +1,151 @@
1
+ # The Mandelbrot set, as an escape-time count.
2
+ #
3
+ # Every cell iterates z = z*z + c until |z| passes 2, and how many passes that
4
+ # took is what is drawn. The number of passes is different for every cell and
5
+ # is not known before the loop runs, which is what `while` is for; the bail-out
6
+ # count is a cap on it, so this is written as an inner loop with a `break`,
7
+ # and the same thing with `while` is measured beside it.
8
+ #
9
+ # Written over whole arrays this would be one pass per iteration over the
10
+ # entire plane, with every cell paying for the deepest one. Here each cell
11
+ # stops when it escapes.
12
+ #
13
+ # Three spellings of the same set are here: the bounded inner loop, the same
14
+ # thing with `while`, and the iteration written in one variable with a Complex
15
+ # local, which is what z = z*z + c says.
16
+ #
17
+ # ruby examples/applications/mandelbrot.rb
18
+
19
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
20
+ require "carray/jit"
21
+
22
+ ROWS, COLUMNS = 36, 96
23
+ LIMIT = 500
24
+
25
+ # The window on the plane: the whole set, squashed to fit a terminal.
26
+ x0, x1 = -2.2, 0.8
27
+ y0, y1 = -1.25, 1.25
28
+
29
+ counts = CArray.int32(ROWS, COLUMNS)
30
+
31
+ escape = lambda do
32
+ CArray.jit_for(ROWS, COLUMNS) { |i, j|
33
+ cx = x0 + (x1 - x0) * j / (COLUMNS - 1.0)
34
+ cy = y0 + (y1 - y0) * i / (ROWS - 1.0)
35
+ zx = 0.0
36
+ zy = 0.0
37
+ taken = 0
38
+ (0...LIMIT).each { |k|
39
+ break if zx * zx + zy * zy > 4.0
40
+ next_zx = zx * zx - zy * zy + cx # a kernel assigns one name at a time
41
+ zy = 2.0 * zx * zy + cy
42
+ zx = next_zx
43
+ taken = taken + 1
44
+ }
45
+ counts[i, j] = taken
46
+ }
47
+ end
48
+
49
+ escape.call
50
+
51
+ # LIMIT passes without escaping is taken for a point of the set.
52
+ SHADES = " .:-=+*#%"
53
+ puts "the set, by escape time"
54
+ counts.to_a.each do |row|
55
+ puts " " + row.map { |count|
56
+ next "@" if count == LIMIT
57
+ step = Math.log(count + 1) / Math.log(LIMIT + 1) * SHADES.size
58
+ SHADES[[step.to_i, SHADES.size - 1].min]
59
+ }.join
60
+ end
61
+
62
+ inside = counts.eq(LIMIT).count(1)
63
+ puts format(" %d of %d cells never escaped", inside, ROWS * COLUMNS)
64
+
65
+ # The same iteration written with `while`, which is what it would be if there
66
+ # were no cap to put in an extent. It gives the same counts; what it gives up
67
+ # is the guarantee that the loop ends, which here is exactly what the cap was
68
+ # providing -- a cell inside the set does not escape, ever, so the `while`
69
+ # needs the count in its condition to stop at all.
70
+ by_while = CArray.int32(ROWS, COLUMNS)
71
+ CArray.jit_for(ROWS, COLUMNS) { |i, j|
72
+ cx = x0 + (x1 - x0) * j / (COLUMNS - 1.0)
73
+ cy = y0 + (y1 - y0) * i / (ROWS - 1.0)
74
+ zx = 0.0
75
+ zy = 0.0
76
+ taken = 0
77
+ while zx * zx + zy * zy <= 4.0 && taken < LIMIT
78
+ next_zx = zx * zx - zy * zy + cx
79
+ zy = 2.0 * zx * zy + cy
80
+ zx = next_zx
81
+ taken = taken + 1
82
+ end
83
+ by_while[i, j] = taken
84
+ }
85
+ puts " while agrees with the bounded loop: #{by_while.to_a == counts.to_a}"
86
+
87
+ # ------------------------------------------------------- and in one variable
88
+
89
+ # The iteration is z = z*z + c, and a kernel can say that. A Complex local
90
+ # is a `double _Complex` in the generated C, so the two reals above collapse
91
+ # into one name and the temporary they needed goes away -- `z = z * z + c` is
92
+ # a single assignment, and multiplication is the one Ruby's Complex does.
93
+ #
94
+ # `Complex(x, y)` is the way in from two reals, `abs` one of the ways back
95
+ # out. Ordering is refused on a Complex, as Ruby refuses it, so the test is
96
+ # on `abs` -- and `abs > 2.0` is the same test as `zx*zx + zy*zy > 4.0`, a
97
+ # square root apart.
98
+ complex_counts = CArray.int32(ROWS, COLUMNS)
99
+
100
+ complex_escape = lambda do
101
+ CArray.jit_for(ROWS, COLUMNS) { |i, j|
102
+ c = Complex(x0 + (x1 - x0) * j / (COLUMNS - 1.0),
103
+ y0 + (y1 - y0) * i / (ROWS - 1.0))
104
+ z = Complex(0.0, 0.0)
105
+ taken = 0
106
+ (0...LIMIT).each { |k|
107
+ break if z.abs > 2.0
108
+ z = z * z + c
109
+ taken = taken + 1
110
+ }
111
+ complex_counts[i, j] = taken
112
+ }
113
+ end
114
+
115
+ complex_escape.call
116
+ puts " the Complex spelling agrees: #{complex_counts.to_a == counts.to_a}"
117
+
118
+ # It costs a square root the real spelling did not pay -- `abs` is `cabs`,
119
+ # where the two reals compared a sum of squares against four -- and the frame
120
+ # below says how much that is, which on this machine is nothing worth
121
+ # choosing between. A kernel that wanted the other test back would write
122
+ # `(z * z.conjugate).real > 4.0`: the same arithmetic, in one name.
123
+ complex_started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
124
+ 10.times { complex_escape.call }
125
+ complex_timed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - complex_started) / 10
126
+
127
+ # What it would have cost in Ruby.
128
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
129
+ 10.times { escape.call }
130
+ compiled = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / 10
131
+
132
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
133
+ (0...ROWS).each do |i|
134
+ (0...COLUMNS).each do |j|
135
+ cx = x0 + (x1 - x0) * j / (COLUMNS - 1.0)
136
+ cy = y0 + (y1 - y0) * i / (ROWS - 1.0)
137
+ zx = zy = 0.0
138
+ taken = 0
139
+ LIMIT.times do
140
+ break if zx * zx + zy * zy > 4.0
141
+ zx, zy = zx * zx - zy * zy + cx, 2.0 * zx * zy + cy
142
+ taken += 1
143
+ end
144
+ by_while[i, j] = taken
145
+ end
146
+ end
147
+ interpreted = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
148
+
149
+ puts format(" one frame: %.2f ms compiled, %.2f ms as a Complex, %.2f ms in Ruby (%.0fx)",
150
+ compiled * 1e3, complex_timed * 1e3, interpreted * 1e3,
151
+ interpreted / compiled)