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,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)
|