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,139 @@
1
+ # Letting CArray drive the loop.
2
+ #
3
+ # An element-wise pass is the one shape that is a *sweep*: nothing reaches a
4
+ # neighbour, nothing chooses an order. So where this CArray has
5
+ # `ca_call_cslab` (3.0.1 and later), jit_each hands it the compiled body
6
+ # and lets it acquire the operands -- broadcasting them, ORing and propagating
7
+ # the masks, and calling back a chunk at a time.
8
+ #
9
+ # Nothing about the expression changes, and nothing here is written
10
+ # differently. What changes is who opens the arrays, and what that costs.
11
+
12
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
13
+ require "carray/jit"
14
+ require "benchmark"
15
+
16
+ Sweep = CArray::JIT::Sweep
17
+ Access = CArray::JIT::Access
18
+
19
+ puts "this CArray has ca_call_cslab: #{Sweep.available?}"
20
+ puts
21
+
22
+ # The rank leaves the loop
23
+ # ------------------------
24
+ # A chunk is one flat run of cells -- and CArray's acquire reads an operand's
25
+ # element count and element size and never looks at its shape, so a chunk is
26
+ # already flat. Nothing is reshaped. What is compiled flat is the *kernel*.
27
+
28
+ def doubled (source, out)
29
+ CArray.jit_each { out = source * 2.0 }
30
+ end
31
+
32
+ flat_in = CArray.double(6).seq!
33
+ flat_out = CArray.double(6)
34
+ cube_in = CArray.double(2, 3, 4).seq!
35
+ cube_out = CArray.double(2, 3, 4)
36
+
37
+ flat = doubled(flat_in, flat_out)
38
+ cube = doubled(cube_in, cube_out)
39
+
40
+ puts "one block, a run of cells and a cube of them"
41
+ puts " loop axes #{flat.rank} and #{cube.rank}"
42
+ puts " arrays keep shape #{flat_out.dim.inspect} and #{cube_out.dim.inspect}"
43
+ puts " one compiled kernel #{flat.equal?(cube)}"
44
+ puts " answers #{flat_out.to_a == (flat_in * 2.0).to_a} and " \
45
+ "#{cube_out.to_a == (cube_in * 2.0).to_a}"
46
+ puts
47
+
48
+ # The wrapper is a call, not a second code generator: the kernel's first three
49
+ # arguments are already base, stride and bounds, which is what a chunk arrives
50
+ # as.
51
+ puts "the sweep entry point"
52
+ puts flat.slab_source.lines
53
+ .grep(/^carray_jit_slab|bounds\[1\] = n|carray_jit_kernel\(base/)
54
+ .map { |line| " " + line.strip }
55
+ puts
56
+
57
+ # What it is worth
58
+ # ----------------
59
+ # An operand CArray cannot walk in place -- a gather, a lazy array -- is
60
+ # re-gathered 32KB at a time by the sweep. The tiers here move the whole box
61
+ # the kernel touches instead, and for an element-wise pass that box is the
62
+ # whole array.
63
+
64
+ n = 2_000_000
65
+ src = CArray.double(n).seq!
66
+ b = CArray.double(n).seq!(0.5, 0.5)
67
+ out = CArray.double(n)
68
+ gather = src[CArray.int32(n).seq.reverse]
69
+
70
+ def timed
71
+ 2.times { yield }
72
+ Benchmark.realtime { 3.times { yield } } / 3
73
+ end
74
+
75
+ puts "n = #{n}, out[] = x + b * 2.0"
76
+ [["entity", src], ["gather view", gather]].each do |label, x|
77
+ elapsed = timed { CArray.jit_each { out = x + b * 2.0 } }
78
+ scratch = Access.classify(x)[:tier] == 3 ? "%.0f MB" % (n * 8.0 / 1024 / 1024) : "none"
79
+ puts " %-12s tier %d %6.2f ms the driver here would hold %s" %
80
+ [label, Access.classify(x)[:tier], elapsed * 1e3, scratch]
81
+ end
82
+ puts " -- the sweep holds 32 KB per gathered operand, whatever n is."
83
+
84
+ # A strided view is the case where the re-gather buys nothing: the tiers
85
+ # address a column, a transpose or every other cell in place, so there is no
86
+ # whole-array copy to be saved from. That is why the driver is chosen on the
87
+ # operands and not only on the shape.
88
+ half = (0...n).step(2)
89
+ strided_x, strided_b, strided_out = src[half], b[half], out[half]
90
+ elapsed = timed { CArray.jit_each { strided_out = strided_x + strided_b * 2.0 } }
91
+ puts " %-12s tier %d %6.2f ms %.2f ns/cell, and no scratch either way" %
92
+ ["strided view", Access.classify(strided_x)[:tier], elapsed * 1e3,
93
+ elapsed * 1e9 / strided_x.elements]
94
+ puts " -- swept, the same pass was 4.7 ns/cell: re-gathered for nothing."
95
+ puts
96
+
97
+ # What keeps the driver here
98
+ # --------------------------
99
+ # Four things, and only the last is about this CArray being old.
100
+
101
+ columns = CArray.double(64, 64).seq!
102
+ into = CArray.double(64, 64)
103
+ every_other = columns[nil, (0...64).step(2)]
104
+ every_other_out = into[nil, (0...64).step(2)]
105
+ kernel = CArray.jit_each { every_other_out = every_other * 2.0 }
106
+ puts "a strided view operand loop axes #{kernel.rank}"
107
+ puts " -- walked in place here, re-gathered there for nothing."
108
+
109
+ matrix = CArray.double(3, 4).seq!
110
+ row = CArray.double(1, 4).seq!(10.0)
111
+ stretched = CArray.double(3, 4)
112
+ kernel = CArray.jit_each { stretched = matrix * row }
113
+ puts "a stretched operand loop axes #{kernel.rank}"
114
+ puts " -- broadcasting arrives as a stride of zero on an axis, and a flat"
115
+ puts " run has no axes, so cell k would stop lining up."
116
+
117
+ masked = CArray.double(5).seq!(1.0)
118
+ masked[2] = UNDEF
119
+ masked_out = CArray.double(5)
120
+ kernel = CArray.jit_each { masked_out = masked * 2.0 }
121
+ puts "a mask anywhere in the pass sweepable? #{kernel.sweepable?}"
122
+ puts " -- an operand that carries one, or a body that asks about one."
123
+ puts " CArray ORs and propagates the masks, but `a[i] == UNDEF` is a"
124
+ puts " question about a single cell, and a chunk carries no per-cell"
125
+ puts " mask to ask it of. The answer is the same either way:"
126
+ puts " #{masked_out.is_masked.to_a.inspect}"
127
+ puts
128
+ puts "a CArray without the family asked for by symbol, not assumed;"
129
+ puts " -- an older one falls back without the caller hearing about it."
130
+ puts
131
+
132
+ # jit_for never goes this way, and that is not a limitation: a kernel that
133
+ # names an index reaches neighbours, chooses an order and runs inner loops,
134
+ # none of which a chunked walk can offer. Splitting the two methods by what
135
+ # the block names drew the same line as what a sweep can drive.
136
+ recurrence = CArray.double(8).seq!(1.0)
137
+ CArray.jit_for(1...8) { |i| recurrence[i] = recurrence[i-1] * 1.5 }
138
+ puts "jit_for stays here by its nature"
139
+ puts " #{recurrence.to_a.map { |v| v.round(4) }.inspect}"
@@ -0,0 +1,80 @@
1
+ # CScalar: a value with a home.
2
+ #
3
+ # A CScalar is the one-cell CArray it subclasses, minus the index. `s[]` is
4
+ # the value and `s[] = ...` puts one back, and that missing index is the whole
5
+ # of the difference -- which is why it needed saying to a compiler whose
6
+ # kernel language is written in subscripts.
7
+
8
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
9
+ require "carray/jit"
10
+
11
+ gain = CScalar.double() { 2.0 }
12
+ offset = CScalar.double() { 10.0 }
13
+ signal = CArray.double(6).seq!(1.0)
14
+ out = CArray.double(6)
15
+
16
+ # In an expression it is read at every cell, which is what CArray's own
17
+ # operators do with it. Nothing here says it is a scalar; it says so itself.
18
+ CArray.jit_each { out = signal * gain + offset }
19
+ puts "signal * gain + offset"
20
+ puts " #{out.to_a.inspect}"
21
+ puts " same as CArray's own operators #{out.to_a == (signal * gain + offset).to_a}"
22
+ puts
23
+
24
+ # And written like any other cell.
25
+ CArray.jit_each { gain = gain + 0.5 }
26
+ puts "gain after gain[] = gain + 0.5 #{gain[0]}"
27
+ puts
28
+
29
+ # An indexed kernel reaches it too, and there the missing index is the point:
30
+ # there is no axis to walk, so there is no index to write. A bare name says
31
+ # the same thing.
32
+ indexed = CArray.double(6)
33
+ CArray.jit_for(6) { |i| indexed[i] = signal[i] * gain[] }
34
+ puts "the same, with the loop written out"
35
+ puts " #{indexed.to_a.inspect}"
36
+
37
+ bare = CArray.double(6)
38
+ CArray.jit_for(6) { |i| bare[i] = signal[i] * gain }
39
+ puts " a bare `gain` agrees #{bare.to_a == indexed.to_a}"
40
+
41
+ # It is still the one-cell array it is, so the index spelling keeps working
42
+ # and means the same cell.
43
+ spelled = CArray.double(6)
44
+ CArray.jit_for(6) { |i| spelled[i] = signal[i] * gain[0] }
45
+ puts " and so does gain[0] #{spelled.to_a == indexed.to_a}"
46
+ puts
47
+
48
+ # Every iteration writes the one cell it has, and what is left is what the
49
+ # same Ruby loop leaves -- which makes it an accumulator without asking for
50
+ # one. The inner loop of a reduction rests on exactly this.
51
+ total = CScalar.int() { 0 }
52
+ counts = CArray.int(4).seq!(1)
53
+ CArray.jit_for(4) { |i| total[] = total[] + counts[i] }
54
+ puts "total after one pass over #{counts.to_a.inspect} #{total[0]}"
55
+
56
+ in_ruby = 0
57
+ (0...4).each { |i| in_ruby = in_ruby + counts[i] }
58
+ puts " what the same Ruby loop leaves #{in_ruby}"
59
+ puts
60
+
61
+ # A contraction takes one as it takes any other operand it does not index.
62
+ left = CArray.double(3).seq!(1.0)
63
+ right = CArray.double(3).seq!(1.0)
64
+ weight = CScalar.double() { 2.0 }
65
+ puts "weighted dot product #{CArray.jit_contract { |k| left[k] * right[k] * weight[] }[0]}"
66
+ puts
67
+
68
+ # What it will not do is take an expression wider than itself, because there
69
+ # is one cell and no answer to which of the values lands. CArray refuses the
70
+ # same assignment, in the same terms.
71
+ begin
72
+ CArray.jit_each { weight = signal + 1.0 }
73
+ rescue CArray::JIT::Unsupported => error
74
+ puts "refused: #{error.message}"
75
+ end
76
+ begin
77
+ weight[] = signal + 1.0
78
+ rescue => error
79
+ puts "CArray: #{error.message.lines.first.strip[0, 78]}"
80
+ end
@@ -0,0 +1,106 @@
1
+ # The window spelling of a stencil, and the border as an argument.
2
+ #
3
+ # 02_stencil.rb writes the same five-point average with the indices named.
4
+ # What that spelling has nowhere to put is the edge: `src[i-1, j]` at i = 0
5
+ # has no meaning, so the extents avoid it and the border keeps whatever the
6
+ # output array held. A window has no index to write and so has somewhere to
7
+ # put the question -- `border:` at the call.
8
+
9
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
10
+ require "carray/jit"
11
+
12
+ rows, columns = 5, 6
13
+ src = CArray.double(rows, columns).seq!(1.0)
14
+
15
+ # The block's parameters are windows onto the arrays given, in that order:
16
+ # a[0, 0] is the cell the loop is on, a[-1, 0] its neighbour. The block's
17
+ # value is what the cell gets, as jit_map's is, and an array of the same shape
18
+ # comes back.
19
+ smoothed = CArray.jit_stencil(src) { |a|
20
+ 0.25 * (a[-1, 0] + a[1, 0] + a[0, -1] + a[0, 1])
21
+ }
22
+
23
+ # The same stencil with the indices named, over the interior alone.
24
+ written = CArray.double(rows, columns)
25
+ CArray.jit_for(1...(rows-1), 1...(columns-1)) { |i, j|
26
+ written[i, j] = 0.25 * (src[i-1, j] + src[i+1, j] + src[i, j-1] + src[i, j+1])
27
+ }
28
+
29
+ puts "the window"
30
+ puts " interior matches jit_for #{smoothed[1..-2, 1..-2].to_a == written[1..-2, 1..-2].to_a}"
31
+ # The default is :mask, because CArray can say "not computed", and a border of
32
+ # zeros that means the same cannot be told from zeros that were computed.
33
+ puts " border missing by default #{smoothed.count_masked} cells"
34
+
35
+ # ---------------------------------------------------------------- the border
36
+
37
+ # Five answers, on one row of a small array so they can be read side by side.
38
+ line = CArray.double(1, 5).seq!(1.0) # 1 2 3 4 5
39
+
40
+ def row (result)
41
+ result.to_a[0].map { |e| e == UNDEF ? " --" : format("%4.1f", e) }.join(" ")
42
+ end
43
+
44
+ puts
45
+ puts "border:, on [1 2 3 4 5] averaging each cell with its two neighbours"
46
+ [:mask, :skip, :zero, :clamp, :wrap].each do |border|
47
+ # :skip leaves the border as it was found, so what it was found as is worth
48
+ # seeing: this one is filled with -1 first.
49
+ into = CArray.double(1, 5).seq!(-1.0, 0.0)
50
+ result = CArray.jit_stencil(line, border: border, into: into) { |a|
51
+ (a[0, -1] + a[0, 0] + a[0, 1]) / 3.0
52
+ }
53
+ puts format(" %-6s %s", border, row(result))
54
+ end
55
+
56
+ # :mask and :skip are answers about the cell -- it is not computed. The other
57
+ # three answer for the read instead, so those cells are computed after all:
58
+ # :zero reads 0 outside, :clamp the nearest cell, :wrap the far side. A Game
59
+ # of Life board is a torus because of :wrap; an image filter usually wants
60
+ # :clamp.
61
+
62
+ # --------------------------------------------------- several arrays and rest
63
+
64
+ # Each array given gets a window, in the order the block names them; the names
65
+ # shadow whatever they hold outside, as CArray.fuse's do. A diffusion step
66
+ # with a conductivity that varies from cell to cell:
67
+ u = CArray.double(6, 6) { |i, j| (i - 2.5).abs + (j - 2.5).abs }
68
+ k = CArray.double(6, 6).fill(0.2)
69
+
70
+ stepped = CArray.jit_stencil(u, k, border: :clamp) { |u, k|
71
+ u[0,0] + k[0,0] * (u[-1,0] + u[1,0] + u[0,-1] + u[0,1] - 4.0 * u[0,0])
72
+ }
73
+ puts
74
+ puts "several arrays"
75
+ puts " every cell computed #{stepped.count_masked.zero?}"
76
+
77
+ # The offsets have to be known before the loop runs -- the radius is what lets
78
+ # the interior be walked without asking, at each cell, whether it is inside.
79
+ # Arithmetic over literals is not a computed offset: it is folded where the
80
+ # block is read, so a stencil drawn from a formula may be written as one.
81
+ wide = CArray.jit_stencil(src, border: :clamp) { |a| a[0, -1-1] + a[0, 1+1] }
82
+ puts " a folded offset #{wide[2, 2] == src[2, 0] + src[2, 4]}"
83
+
84
+ # What may not appear is anything that has to be read to be known -- a
85
+ # captured integer included, since one compiled kernel serves every value of
86
+ # it and the loop would not know the radius.
87
+ begin
88
+ offset = 1
89
+ CArray.jit_stencil(src) { |a| a[0, offset] }
90
+ rescue CArray::JIT::Unsupported => error
91
+ puts " a computed offset refused: #{error.message.sub(/ \(at line.*/m, "")}"
92
+ end
93
+
94
+ # ------------------------------------------------------ the array comes back
95
+
96
+ # Typed from the block's value unless you say otherwise, as jit_map's is.
97
+ counts = CArray.jit_stencil(src, border: :zero, type: :int32) { |a|
98
+ a[-1, 0] + a[1, 0] > a[0, 0] ? 1 : 0
99
+ }
100
+ puts
101
+ puts "the result"
102
+ puts " type: #{counts.data_type_name}"
103
+ # into: writes an array of yours and returns it; the type is then that array's.
104
+ mine = CArray.float32(rows, columns)
105
+ same = CArray.jit_stencil(src, border: :clamp, into: mine) { |a| a[0, 0] * 2.0 }
106
+ puts " into: #{same.equal?(mine)}, and typed #{mine.data_type_name} by it"
@@ -0,0 +1,148 @@
1
+ # Loops inside a kernel: an inner `each` with a `break`, and `while` where the
2
+ # bound is not knowable.
3
+ #
4
+ # The kernel's own loop is written by the extents. Inside the body a cell may
5
+ # still need a loop of its own -- a search, an iteration to a tolerance -- and
6
+ # there are two spellings, which differ in whether the number of passes can be
7
+ # bounded before the loop runs.
8
+
9
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
10
+ require "carray/jit"
11
+
12
+ rows, columns = 5, 8
13
+ source = CArray.double(rows, columns) { |i, j| (j - i) * 1.0 }
14
+ threshold = 1.5
15
+
16
+ # A search: the first column past the threshold, or -1. `break` in an inner
17
+ # loop means what it means in Ruby, and so does `next`.
18
+ first = CArray.int32(rows)
19
+ CArray.jit_for(rows) { |i|
20
+ found = -1
21
+ (0...columns).each { |j|
22
+ if source[i, j] > threshold
23
+ found = j
24
+ break
25
+ end
26
+ }
27
+ first[i] = found
28
+ }
29
+
30
+ reference = (0...rows).map { |i|
31
+ (0...columns).find { |j| source[i, j] > threshold } || -1
32
+ }
33
+
34
+ puts "an inner loop with a break"
35
+ puts " first past #{threshold} #{first.to_a.inspect}"
36
+ puts " matches Ruby #{first.to_a == reference}"
37
+
38
+ # ------------------------------------------------------- a bounded iteration
39
+
40
+ # Newton's method for a square root, with the bound in the extent. This is
41
+ # the better spelling wherever a bound exists: the kernel cannot fail to stop,
42
+ # and a cell that ran out of passes can be told from one that converged.
43
+ n = 2000
44
+ a = CArray.double(n).seq!(1.0)
45
+ tolerance = 1e-12
46
+ cap = 40
47
+
48
+ root = CArray.double(n)
49
+ passes = CArray.int32(n)
50
+ CArray.jit_for(n) { |i|
51
+ x = a[i]
52
+ taken = 0
53
+ (0...cap).each { |k|
54
+ break if (x * x - a[i]).abs <= tolerance * a[i]
55
+ x = 0.5 * (x + a[i] / x)
56
+ taken = taken + 1
57
+ }
58
+ root[i] = x
59
+ passes[i] = taken
60
+ }
61
+
62
+ puts
63
+ puts "Newton, bounded by the extent"
64
+ puts format(" worst relative error %.1e", ((root ** 2 - a).abs / a).max)
65
+ puts " passes taken #{passes.min}..#{passes.max}"
66
+ puts " none ran out #{passes.max < cap}"
67
+
68
+ # ----------------------------------------------------------------- and while
69
+
70
+ # Where the bound is not knowable, `while` says so. The condition is read at
71
+ # the top of every pass, as Ruby's is; a local the condition reads has to be a
72
+ # local before the loop, since the condition is read before the body is.
73
+ by_while = CArray.double(n)
74
+ CArray.jit_for(n) { |i|
75
+ guess = a[i]
76
+ while (guess * guess - a[i]).abs > tolerance * a[i]
77
+ guess = 0.5 * (guess + a[i] / guess)
78
+ end
79
+ by_while[i] = guess
80
+ }
81
+
82
+ puts
83
+ puts "while"
84
+ puts " same answers #{by_while.to_a == root.to_a}"
85
+
86
+ # What it gives up is the guarantee that the loop ends -- nothing here can
87
+ # decide that in general, and a kernel that does not return cannot be
88
+ # interrupted, because the generated loop has no place to notice a signal.
89
+ # The one runaway that can be read off the page is refused rather than
90
+ # compiled:
91
+ begin
92
+ CArray.jit_for(4) { |i| while true do root[i] = root[i] + 1.0 end }
93
+ rescue CArray::JIT::Unsupported => error
94
+ puts " while true, no break refused: #{error.message.sub(/ \(at line.*/m, "")}"
95
+ end
96
+ # `while true` with a `break` in it is an ordinary thing to write, and is left
97
+ # alone. `until` is not in the subset: `while` with the condition negated is
98
+ # the same loop, and one spelling is enough to keep.
99
+
100
+ # ------------------------------------------------------------------- and next
101
+
102
+ # `next` in the kernel block skips the cell, the way it would end a block Ruby
103
+ # was running: the cell keeps its value and its mask, and the loop moves on.
104
+ kept = CArray.double(n).fill(-1.0)
105
+ CArray.jit_for(n) { |i|
106
+ next if a[i] % 2.0 == 0.0
107
+ kept[i] = a[i]
108
+ }
109
+ puts
110
+ puts "next in the kernel block"
111
+ puts " even cells untouched #{(0...8).map { |i| kept[i] }.inspect}"
112
+
113
+ # What it costs: an inner loop carrying a break is not a fold, so it is not
114
+ # split into partial sums -- it stays the serial chain it was. The kernel is
115
+ # compiled on its first call and cached, so it is run once before the clock
116
+ # starts.
117
+ newton = lambda do
118
+ CArray.jit_for(n) { |i|
119
+ x = a[i]
120
+ (0...cap).each { |k|
121
+ break if (x * x - a[i]).abs <= tolerance * a[i]
122
+ x = 0.5 * (x + a[i] / x)
123
+ }
124
+ root[i] = x
125
+ }
126
+ end
127
+ newton.call
128
+
129
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
130
+ 20.times { newton.call }
131
+ compiled = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / 20
132
+
133
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
134
+ 20.times do
135
+ (0...n).each do |i|
136
+ x = a[i]
137
+ cap.times do
138
+ break if (x * x - a[i]).abs <= tolerance * a[i]
139
+ x = 0.5 * (x + a[i] / x)
140
+ end
141
+ root[i] = x
142
+ end
143
+ end
144
+ interpreted = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / 20
145
+
146
+ puts
147
+ puts format("Newton over %d cells: %.3f ms compiled, %.3f ms in Ruby (%.0fx)",
148
+ n, compiled * 1e3, interpreted * 1e3, interpreted / compiled)
@@ -0,0 +1,69 @@
1
+ # Raising from a kernel.
2
+ #
3
+ # A kernel can stop and say why. What comes back is a RuntimeError with the
4
+ # message written in the block -- what `raise "..."` gives in Ruby -- and the
5
+ # loop stops where it raised: the cell that raised is not written, and neither
6
+ # are the ones after it.
7
+
8
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
9
+ require "carray/jit"
10
+
11
+ n = 8
12
+ depth = CArray.double(n).seq!(3.0, -1.0) # 3 2 1 0 -1 -2 ...
13
+ out = CArray.double(n).fill(-99.0)
14
+
15
+ begin
16
+ CArray.jit_for(n) { |i|
17
+ raise "depth went negative" if depth[i] < 0.0
18
+ out[i] = Math.sqrt(depth[i])
19
+ }
20
+ rescue RuntimeError => error
21
+ puts "raising"
22
+ puts " class #{error.class}"
23
+ puts " message #{error.message}"
24
+ end
25
+
26
+ # The cells before the raise keep what the kernel wrote, as they do when a
27
+ # division with no divisor stops one.
28
+ puts " written before it #{out.to_a[0, 4].map { |e| e.round(3) }.inspect}"
29
+ puts " untouched after it #{out.to_a[4..].all?(-99.0)}"
30
+
31
+ # The message is written out and the class is not named, and both follow from
32
+ # where the message goes: C has nothing to carry a string out of a cell in, so
33
+ # the message is registered as the kernel is compiled and the cell writes a
34
+ # code for it into the error slot the kernel already watches. The raise
35
+ # itself happens once the loop has stopped and there is a Ruby stack to raise
36
+ # on.
37
+
38
+ # A cell whose value is missing does not raise: the comparison was decided by
39
+ # bytes that mean nothing, and this is the rule the division helper already
40
+ # keeps and that `if` keeps for what it writes.
41
+ holed = CArray.double(4).seq!(1.0)
42
+ holed[2] = 999.0 # a value that would raise below
43
+ holed[2] = UNDEF # marked missing; the bytes are left alone
44
+ squares = CArray.double(4)
45
+ CArray.jit_for(4) { |i|
46
+ raise "out of range" if holed[i] > 100.0
47
+ squares[i] = holed[i] * holed[i]
48
+ }
49
+ puts " a masked cell did not raise on its 999.0; #{squares.count_masked} cell came back missing"
50
+
51
+ # A jit_function body raises the same way, and the message travels with the
52
+ # function: f.call(-1.0) and the same body reached from a kernel raise the
53
+ # same thing.
54
+ safe_sqrt = CArray.jit_function("double (*)(double)") { |x|
55
+ raise "negative argument" if x < 0.0
56
+ Math.sqrt(x)
57
+ }
58
+
59
+ begin
60
+ safe_sqrt.call(-1.0)
61
+ rescue RuntimeError => error
62
+ puts " from jit_function #{error.message} (called from Ruby)"
63
+ end
64
+
65
+ begin
66
+ CArray.jit_for(n) { |i| out[i] = safe_sqrt.call(depth[i]) }
67
+ rescue RuntimeError => error
68
+ puts " from a kernel #{error.message} (the same message, pasted in)"
69
+ end