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,90 @@
1
+ # Reductions: an inner loop per output cell.
2
+ #
3
+ # The caller says where the answer goes and the kernel fills that cell. What
4
+ # makes it expressible is `(from...to).each { |j| ... }` -- an inner loop whose
5
+ # index addresses arrays but writes nothing -- so sum, maximum, product, count
6
+ # and a dot product all fall out of ordinary block-locals, with no primitive
7
+ # for any of them. `n.times { |j| ... }` is the same loop from zero.
8
+
9
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
10
+ require "carray/jit"
11
+
12
+ rows, columns = 5, 7
13
+ source = CArray.double(rows, columns).seq!(1.0)
14
+ total = CArray.double(rows)
15
+ largest = CArray.double(rows)
16
+ positives = CArray.int32(rows)
17
+
18
+ CArray.jit_for(rows) { |i|
19
+ accumulator = 0.0
20
+ (0...columns).each { |j| accumulator = accumulator + source[i, j] }
21
+ total[i] = accumulator
22
+ }
23
+
24
+ CArray.jit_for(rows) { |i|
25
+ best = source[i, 0]
26
+ (1...columns).each { |j|
27
+ if source[i, j] > best
28
+ best = source[i, j]
29
+ end
30
+ }
31
+ largest[i] = best
32
+ }
33
+
34
+ CArray.jit_for(rows) { |i|
35
+ count = 0
36
+ (0...columns).each { |j|
37
+ if source[i, j] > 20.0
38
+ count = count + 1
39
+ end
40
+ }
41
+ positives[i] = count
42
+ }
43
+
44
+ puts "row reductions"
45
+ puts " sum #{total.to_a.inspect}"
46
+ puts " matches #{total.to_a == source.to_a.map { |row| row.sum }}"
47
+ puts " max #{largest.to_a.inspect}"
48
+ puts " count #{positives.to_a.inspect}"
49
+
50
+ # A matrix multiply is the same shape of thing: two output indices and one
51
+ # inner loop.
52
+ left = CArray.double(3, 4).seq!(1.0)
53
+ right = CArray.double(4, 2).seq!(0.5, 0.5)
54
+ product = CArray.double(3, 2)
55
+
56
+ CArray.jit_for(3, 2) { |i, j|
57
+ accumulator = 0.0
58
+ (0...4).each { |t| accumulator = accumulator + left[i, t] * right[t, j] }
59
+ product[i, j] = accumulator
60
+ }
61
+
62
+ reference = Array.new(3) { |i| Array.new(2) { |j|
63
+ (0...4).sum { |t| left[i, t] * right[t, j] } } }
64
+
65
+ puts " matmul #{product.to_a.inspect}"
66
+ puts " matches #{product.to_a == reference}"
67
+
68
+ # The accumulator is split into partial sums, as sum(axis:) splits its own:
69
+ # a serial chain waits out the latency of each addition, and the split answer
70
+ # is usually the more accurate one as well as the faster. It is not the order
71
+ # the same loop takes in Ruby, and `reassociate: false` is what asks for that.
72
+ #
73
+ # CArray's own reductions are faster than this still, their kernels being
74
+ # written for the shape. What this is for is the reductions that have no such
75
+ # method -- the ones where the body is an algorithm rather than an operator.
76
+ puts " note CArray#sum(axis: 1) is the faster way to write the first one"
77
+ puts
78
+
79
+ # Counting from zero is what an inner loop mostly does, and `n.times` says it
80
+ # without naming an end. It compiles to the loop the range spells out --
81
+ # `(0...n).each` -- so the choice is the reader's, not the compiler's.
82
+ counted = CArray.double(rows)
83
+ CArray.jit_for(rows) { |i|
84
+ accumulator = 0.0
85
+ columns.times { |j| accumulator = accumulator + source[i, j] }
86
+ counted[i] = accumulator
87
+ }
88
+ puts "the same sums, written with times"
89
+ puts " #{counted.to_a.inspect}"
90
+ puts " matches the range spelling #{counted.to_a == total.to_a}"
@@ -0,0 +1,58 @@
1
+ # Contractions: an index that appears twice is summed.
2
+ #
3
+ # An index that appears twice in the term is summed -- the repetition is what
4
+ # stands in for the sigma. No extents are given, because each index's extent
5
+ # is fixed by the axes it addresses, and an index whose axes disagree is
6
+ # refused: that shape check is what a contraction is for.
7
+
8
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
9
+ require "carray/jit"
10
+
11
+ a = CArray.double(3, 4).seq!(1.0)
12
+ b = CArray.double(4, 2).seq!(0.5, 0.5)
13
+ v = CArray.double(4).seq!(1.0)
14
+ p = CArray.double(3).seq!(1.0)
15
+ r = CArray.double(2).seq!(10.0, 10.0)
16
+ q = CArray.double(3, 3).seq!(1.0)
17
+
18
+ matmul = CArray.jit_contract { |i, j, k| a[i,k] * b[k,j] } # k is summed
19
+ matvec = CArray.jit_contract { |i, k| a[i,k] * v[k] } # k is summed
20
+ dot = CArray.jit_contract { |i, k| a[i,k] * a[i,k] } # both, one cell
21
+ trace = CArray.jit_contract { |i| q[i,i] } # one array, twice
22
+ outer = CArray.jit_contract { |i, j| p[i] * r[j] } # nothing summed
23
+
24
+ puts "contractions"
25
+ puts " a . b #{matmul.to_a.inspect}"
26
+ puts " a . v #{matvec.to_a.inspect}"
27
+ puts " |a|^2 #{dot[0]} (#{a.to_a.flatten.sum { |e| e * e }})"
28
+ puts " tr q #{trace[0]}"
29
+ puts " p (x) r #{outer.to_a.inspect}"
30
+
31
+ # The result's axes are the free indices in the order the block named them, so
32
+ # the parameter list is where the axis order is stated -- and swapping two
33
+ # parameters transposes.
34
+ transposed = CArray.jit_contract { |j, i, k| a[i,k] * b[k,j] }
35
+ puts " swapped #{transposed.dim.inspect} vs #{matmul.dim.inspect}"
36
+
37
+ # Assigning into an array of your own says where to put it instead. It must
38
+ # name exactly the free indices, and still does not decide what is summed.
39
+ destination = CArray.double(3, 2)
40
+ CArray.jit_contract { |i, j, k| destination[i,j] = a[i,k] * b[k,j] }
41
+ puts " into mine #{destination.to_a == matmul.to_a}"
42
+
43
+ # A sum along an axis is not a contraction: there is nothing in a[i,k]
44
+ # standing in for a sigma.
45
+ begin
46
+ total = CArray.double(3)
47
+ CArray.jit_contract { |i, k| total[i] = a[i,k] }
48
+ rescue CArray::JIT::Unsupported => error
49
+ puts " refused: #{error.message.lines.first.strip}"
50
+ end
51
+
52
+ # Nor is a shape mismatch let through.
53
+ begin
54
+ wrong = CArray.double(5, 2)
55
+ CArray.jit_contract { |i, j, k| a[i,k] * wrong[k,j] }
56
+ rescue CArray::JIT::Unsupported => error
57
+ puts " refused: #{error.message.lines.first.strip}"
58
+ end
@@ -0,0 +1,55 @@
1
+ # Masks: cells that hold no value.
2
+ #
3
+ # `a[i] == UNDEF` is how Ruby already asks whether a cell is missing, and it
4
+ # means the same here -- it reads the mask byte, never the value. Asking about
5
+ # the mask is not reading the value, so what the branch writes carries no mask,
6
+ # which is what makes filling a hole possible.
7
+
8
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
9
+ require "carray/jit"
10
+
11
+ n = 7
12
+ source = CArray.double(n).seq!(1.0)
13
+ source[2] = UNDEF
14
+
15
+ # Filling the holes. This is an ordinary Ruby loop as well, so it can be
16
+ # checked against one.
17
+ filled = CArray.double(n)
18
+ CArray.jit_for(n) { |i|
19
+ if source[i] == UNDEF
20
+ filled[i] = 0.0
21
+ else
22
+ filled[i] = Math.sqrt(source[i])
23
+ end
24
+ }
25
+
26
+ reference = (0...n).map { |i| source[i] == UNDEF ? 0.0 : Math.sqrt(source[i]) }
27
+
28
+ puts "masks"
29
+ puts " filled #{filled.to_a.map { |e| e.round(4) }.inspect}"
30
+ puts " matches Ruby #{filled.to_a == reference}"
31
+ # The array that was written gets a mask because the kernel is a masked one;
32
+ # what matters is that no cell in it is missing.
33
+ puts " cells missing #{filled.count_masked} (a mask exists, but it is empty)"
34
+
35
+ # Left implicit, the mask propagates as CArray's own operators propagate it:
36
+ # any cell that fed a result masks that result, following the offsets, so each
37
+ # output takes its mask from its own inputs rather than from everything the
38
+ # body read.
39
+ neighbours = CArray.double(n)
40
+ CArray.jit_for(1...(n-1)) { |i| neighbours[i] = source[i-1] + source[i+1] }
41
+ puts " propagated #{neighbours.to_a.inspect}"
42
+ puts " masked where #{(0...n).select { |i| neighbours[i] == UNDEF }.inspect} (the cells reading source[2])"
43
+
44
+ # Writing UNDEF marks a cell missing. Mentioning UNDEF at all makes the kernel
45
+ # a masked one, whatever its arrays happen to carry.
46
+ clipped = CArray.double(n).seq!(1.0)
47
+ CArray.jit_for(n) { |i|
48
+ if clipped[i] > 4.0
49
+ clipped[i] = UNDEF
50
+ end
51
+ }
52
+ puts " marked missing #{clipped.to_a.inspect}"
53
+
54
+ # A branch with no else writes nothing on the path not taken, so those cells
55
+ # keep both their value and their mask -- as the same `if` would in Ruby.
@@ -0,0 +1,46 @@
1
+ # Views are written in place, without a copy.
2
+ #
3
+ # A column of a matrix, a transpose, a slice of a slice: these are the things
4
+ # views exist for, and a kernel that only accepted a contiguous array would
5
+ # make the caller copy them in and out again. Where a view folds to a stride
6
+ # expression -- which a column, a reversal and a transpose all do -- the kernel
7
+ # addresses the original memory and the write lands in the parent.
8
+
9
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
10
+ require "carray/jit"
11
+
12
+ # A column: its stride is the row pitch, not the element size.
13
+ matrix = CArray.double(8, 3)
14
+ column = matrix[nil, 1]
15
+ column[0] = 1.0
16
+
17
+ CArray.jit_for(1...8) { |i| column[i] = column[i-1] * 2.0 + 1.0 }
18
+
19
+ puts "views"
20
+ puts " column written #{matrix[nil, 1].to_a.inspect}"
21
+ puts " others untouched #{matrix[nil, 0].to_a.all?(0.0) && matrix[nil, 2].to_a.all?(0.0)}"
22
+
23
+ # A reversed view has a negative stride, and a slice of a slice folds to one
24
+ # offset and one stride however many times it was sliced.
25
+ array = CArray.double(20).seq!
26
+ inner = array[4..15][2..9]
27
+ CArray.jit_for(8) { |i| inner[i] = -inner[i] }
28
+ puts " slice of a slice #{array[5..14].to_a.inspect}"
29
+
30
+ # A transpose is read at its own indices; nothing is materialised.
31
+ source = CArray.double(4, 3).seq!(1.0)
32
+ transposed = source.transpose
33
+ result = CArray.double(3, 4)
34
+ CArray.jit_for(*transposed.dim) { |i, j| result[i, j] = transposed[i, j] * 2.0 }
35
+ puts " transposed read #{result.to_a == (source.transpose * 2.0).to_a}"
36
+
37
+ # A view with no stride expression -- a gather, say -- cannot be addressed
38
+ # arithmetically, so the cells the kernel asked for are transferred and written
39
+ # back. The cost is proportional to the region, not to the view: the kernel
40
+ # knows its extents and its offsets before it runs, so it asks for the box it
41
+ # will touch and no more.
42
+ whole = CArray.double(64).seq!
43
+ gathered = whole[whole >= 0.0]
44
+ gathered[10] = 1.0
45
+ CArray.jit_for(11...14) { |i| gathered[i] = gathered[i-1] * 2.0 }
46
+ puts " gather region #{whole[9..14].to_a.inspect} (9 and 14 outside the box)"
@@ -0,0 +1,55 @@
1
+ # Seeing what was compiled, and what is refused.
2
+ #
3
+ # The generated C is the debugging surface: `jit_for` returns the kernel, and
4
+ # the kernel carries its source. Running with CARRAY_JIT_DUMP=1 prints the
5
+ # same thing for every kernel as it is compiled, and `carray-jit list` / `show`
6
+ # report what is cached on disk between runs.
7
+
8
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
9
+ require "carray/jit"
10
+
11
+ values = CArray.double(16).seq!(1.0)
12
+
13
+ def sweep (values)
14
+ CArray.jit_for(1...values.dim[0]) { |i| values[i] = Math.sqrt(values[i-1]) + 1.0 }
15
+ end
16
+
17
+ kernel = sweep(values)
18
+
19
+ # The contiguous loop, as it was handed to the compiler.
20
+ puts "generated C"
21
+ puts kernel.c_source[/^static void\ncarray_jit_contiguous.*?^\}$/m].lines.map { |line| " " + line.rstrip }
22
+
23
+ # Every kernel carries two loops and decides between them once, outside the
24
+ # loop: the contiguous form indexes a typed pointer and can be vectorised, the
25
+ # strided form is what lets a view run without being copied first.
26
+ puts " both loops present #{%w[carray_jit_contiguous carray_jit_strided].all? { |name| kernel.c_source.include?(name) }}"
27
+
28
+ # Compiling happens once. The second call finds the object already loaded, the
29
+ # second run of the script finds it on disk.
30
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
31
+ sweep(values)
32
+ again = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
33
+ puts " second call #{format('%.4f ms', again * 1e3)} (the same block, already loaded)"
34
+
35
+ # Anything outside the subset raises rather than falling back, and says where.
36
+ # A silent fallback would turn a typo into a performance mystery.
37
+ puts
38
+ puts "refused"
39
+ [
40
+ proc { CArray.jit_for(16) { |i| values[i] = values.to_a.max } },
41
+ proc { CArray.jit_for(16) { |i| values[i] = "text" } },
42
+ proc { n = 0; CArray.jit_for(16) { |i| while n < 3 do n += 1 end } },
43
+ ].each do |attempt|
44
+ begin
45
+ attempt.call
46
+ rescue CArray::JIT::Unsupported => error
47
+ puts " #{error.message.lines.first.strip}"
48
+ end
49
+ end
50
+
51
+ puts
52
+ puts "the cache lives under #{CArray::JIT::Compiler.cache_directory}"
53
+ puts " carray-jit list what is cached for this environment"
54
+ puts " carray-jit show <prefix> the C source of one cached kernel"
55
+ puts " carray-jit clear remove it"
@@ -0,0 +1,107 @@
1
+ # Complex arrays: cmplx64 and cmplx128, computed in `double _Complex`.
2
+ #
3
+ # The interesting part is not that it works but what it costs to make the
4
+ # answer Ruby's rather than C's: Ruby's Complex arithmetic differs from the C
5
+ # operators in the last bit and in the sign of a zero, so three of the four
6
+ # operators are compiled to match Ruby rather than to match C.
7
+
8
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
9
+ require "carray/jit"
10
+
11
+ n = 8
12
+ # A spiral, so that the magnitude has something to say.
13
+ signal = CArray.cmplx128(n) { |i|
14
+ Complex(Math.cos(i * 0.7), Math.sin(i * 0.7)) * (1.0 + 0.25 * i)
15
+ }
16
+
17
+ # Arithmetic, a captured Complex scalar, and an imaginary literal.
18
+ rotation = Complex(0.0, 1.0)
19
+ rotated = CArray.cmplx128(n)
20
+ CArray.jit_for(n) { |i| rotated[i] = signal[i] * rotation + 0.5i }
21
+
22
+ reference = (0...n).map { |i| signal[i] * rotation + 0.5i }
23
+ puts "complex arithmetic"
24
+ puts " matches Ruby #{(0...n).all? { |i| rotated[i] == reference[i] }}"
25
+
26
+ # The way out of the complex type. `abs`, `real`, `imag` and `arg` hand back
27
+ # a Float, which is what lets a complex kernel write into a real array;
28
+ # `conjugate` stays complex.
29
+ power = CArray.double(n)
30
+ phase = CArray.double(n)
31
+ CArray.jit_for(n) { |i| power[i] = signal[i].abs }
32
+ CArray.jit_for(n) { |i| phase[i] = signal[i].arg }
33
+ puts " power #{power.to_a.map { |e| e.round(4) }.inspect}"
34
+ puts " phase #{phase.to_a.map { |e| e.round(4) }.inspect}"
35
+
36
+ # And the way in, from two real arrays.
37
+ real = CArray.double(n).seq!(1.0)
38
+ imaginary = CArray.double(n).seq!(0.0, 0.25)
39
+ built = CArray.cmplx128(n)
40
+ CArray.jit_for(n) { |i| built[i] = Complex(real[i], imaginary[i]) }
41
+ puts " built from parts #{built.to_a.first(3).inspect}"
42
+
43
+ # The fifteen functions a complex CArray answers compile to their C99
44
+ # c-prefixed forms. Which fifteen is CArray's list, not a list invented here,
45
+ # so a formula gives the same answer whichever way it is applied.
46
+ transformed = CArray.cmplx128(n)
47
+ CArray.jit_for(n) { |i| transformed[i] = signal[i].tanh }
48
+ puts " tanh matches the array operator #{transformed.to_a == signal.tanh.to_a}"
49
+
50
+ # A reduction over complex cells: the accumulator starts from a complex zero.
51
+ total = CArray.cmplx128(1)
52
+ CArray.jit_for(1) { |i|
53
+ running = Complex(0.0, 0.0)
54
+ (0...n).each { |j| running = running + signal[j] }
55
+ total[i] = running
56
+ }
57
+ serial = (0...n).inject(Complex(0.0, 0.0)) { |sum, j| sum + signal[j] }
58
+ puts " sum #{total[0].rectangular.map { |e| e.round(6) }.inspect}"
59
+ puts " matches Ruby #{total[0] == serial}"
60
+
61
+ # Where Ruby and C part company. A real operand carries an exact Integer zero
62
+ # as its imaginary part, and Ruby's own arithmetic returns the other operand
63
+ # untouched rather than combining with it -- so the sign of a zero survives an
64
+ # addition that C's `+` would flatten.
65
+ edge = CArray.cmplx128(1)
66
+ edge[0] = Complex(1.0, -0.0)
67
+ kept = CArray.cmplx128(1)
68
+ CArray.jit_for(1) { |i| kept[i] = edge[i] + 2.0 }
69
+ puts
70
+ puts "signed zeros"
71
+ puts " Ruby #{(Complex(1.0, -0.0) + 2.0)}"
72
+ puts " the kernel #{kept[0]}"
73
+ puts " widening first #{Complex(1.0, -0.0) + Complex(2.0, 0.0)} <- what C's + would give"
74
+
75
+ # `**` is the one operation whose answer is not the Ruby loop's to the last
76
+ # bit: Ruby raises a Complex to a power by binary powering, cpow goes round
77
+ # through exp and log. A few machine epsilons apart, growing with the
78
+ # exponent -- and `z * z` is both exact and cheaper than the library call.
79
+ cubed = CArray.cmplx128(n)
80
+ CArray.jit_for(n) { |i| cubed[i] = signal[i] ** 3 }
81
+ worst = (0...n).map { |i| e = signal[i] ** 3; (cubed[i] - e).abs / e.abs }.max
82
+
83
+ squared = CArray.cmplx128(n)
84
+ CArray.jit_for(n) { |i| squared[i] = signal[i] * signal[i] }
85
+
86
+ puts
87
+ puts "the one inexact operation"
88
+ puts " z ** 3 worst #{(worst / Float::EPSILON).round(1)} machine epsilons"
89
+ puts " z * z matches Ruby exactly #{(0...n).all? { |i| squared[i] == signal[i] * signal[i] }}"
90
+
91
+ # What is refused on a Complex is what Ruby refuses, and nothing else: the
92
+ # ordering comparisons, `%`, the rounding methods and the bit operators.
93
+ puts
94
+ puts "refused, as Ruby refuses them"
95
+ [["->(i) { power[i] = signal[i] < 1.0 ? 1.0 : 0.0 }",
96
+ proc { CArray.jit_for(n) { |i| power[i] = signal[i] < 1.0 ? 1.0 : 0.0 } }],
97
+ ["->(i) { power[i] = signal[i].floor }",
98
+ proc { CArray.jit_for(n) { |i| power[i] = signal[i].floor } }],
99
+ ["->(i) { power[i] = signal[i] }",
100
+ proc { CArray.jit_for(n) { |i| power[i] = signal[i] } }]].each do |written, run|
101
+ begin
102
+ run.call
103
+ rescue CArray::JIT::Unsupported => error
104
+ puts " #{written}"
105
+ puts " #{error.message.sub(/ \(at line.*/, '')}"
106
+ end
107
+ end
@@ -0,0 +1,260 @@
1
+ # C functions in a kernel: the ones already compiled, and the ones written
2
+ # here.
3
+ #
4
+ # math.h is already handled -- `Math.sqrt(x)` compiles to `sqrt(x)` and the
5
+ # compiler can inline it. This is for everything else: the Bessel functions
6
+ # in libm that Ruby has no Math method for, and, the same way, anything in a
7
+ # library you can dlopen.
8
+ #
9
+ # Two methods, because they do two different things. `jit_extern` finds one
10
+ # someone else compiled -- `extern` is C's word for a body that lives
11
+ # elsewhere, and finding it is Fiddle's job, with no compiler involved.
12
+ # `jit_function` compiles a body of your own. What comes back is the same
13
+ # kind of object either way, so a kernel calls it without knowing which it is.
14
+ #
15
+ # The *call* is not Fiddle's, either way: reaching a function through
16
+ # Fiddle::Function costs a few hundred nanoseconds per cell, which is more
17
+ # than the arithmetic it was called for. Fiddle is asked where the function
18
+ # is; the kernel calls it.
19
+
20
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
21
+ require "carray/jit"
22
+ require "benchmark"
23
+
24
+ # The prototype is what you would copy out of the header. With no `from:`,
25
+ # the symbol is looked for in what the process has already loaded, which is
26
+ # where libm's own functions are.
27
+ j0 = CArray.jit_extern("double j0(double)")
28
+ puts "bound: #{j0}"
29
+
30
+ x = CArray.double(6).seq!(1.0)
31
+ out = CArray.double(6)
32
+
33
+ CArray.jit_each { out = j0.call(x) }
34
+ puts "j0(x) #{out.to_a.map { |v| v.round(6) }.inspect}"
35
+ puts "same in Ruby #{x.to_a.map { |v| j0.call(v).round(6) }.inspect}"
36
+ puts
37
+
38
+ # What "apply f to an array" cannot say
39
+ # -------------------------------------
40
+ # A stencil needs the function at two places at once. There is no array of
41
+ # intermediate results to hold them, so this is not expressible as a map --
42
+ # but it is an ordinary kernel.
43
+
44
+ smoothed = CArray.double(6)
45
+ CArray.jit_for(1...6) { |i| smoothed[i] = 0.5 * (j0.call(x[i]) + j0.call(x[i-1])) }
46
+ puts "half-sum of neighbouring j0"
47
+ puts " #{smoothed[1..5].to_a.map { |v| v.round(6) }.inspect}"
48
+ puts
49
+
50
+ # It mixes into an expression like anything else, and takes as many arguments
51
+ # as the prototype says.
52
+ atan2 = CArray.jit_extern("double atan2(double, double)")
53
+ y = CArray.double(6).seq!(0.5, 0.5)
54
+ mixed = CArray.double(6)
55
+ CArray.jit_each { mixed = atan2.call(x, y) * 2.0 - j0.call(x) }
56
+ puts "atan2(x, y) * 2 - j0(x)"
57
+ puts " #{mixed.to_a.map { |v| v.round(6) }.inspect}"
58
+ puts
59
+
60
+ # One kernel, every function of that shape
61
+ # ----------------------------------------
62
+ # The kernel is compiled for the *signature*, not the symbol: the address
63
+ # travels in a buffer beside the captured scalars rather than being linked
64
+ # against. So the block below is compiled once and serves all three, and
65
+ # nothing in the generated C says which library any of them came from.
66
+
67
+ def apply (f, x, out)
68
+ CArray.jit_each { out = f.call(x) }
69
+ end
70
+
71
+ kernels = ["double j0(double)", "double y0(double)", "double tgamma(double)"]
72
+ .map { |prototype|
73
+ f = CArray.jit_extern(prototype)
74
+ kernel = apply(f, x, out)
75
+ puts " %-24s -> %s" % [prototype, out[0..2].to_a.map { |v| v.round(6) }.inspect]
76
+ kernel
77
+ }
78
+ puts "compiled kernels: #{kernels.uniq.size} for #{kernels.size} functions"
79
+ puts
80
+
81
+ puts kernels.first.c_source.lines.grep(/typedef|functions\[0\]/).map(&:strip).uniq
82
+ puts
83
+
84
+ # A function of your own
85
+ # ----------------------
86
+ # `jit_function`: the same C declaration, written anonymously.
87
+ # `double (*)(double)` is the spelling C already has for the type of a
88
+ # function pointer, which is what this hands out. There is no name because
89
+ # nothing links by name -- the address travels in a buffer -- so a name would
90
+ # have been invented to be looked at once.
91
+
92
+ smoothstep = CArray.jit_function("double (*)(double)") { |t|
93
+ clamped = t < 0.0 ? 0.0 : (t > 1.0 ? 1.0 : t)
94
+ clamped * clamped * (3.0 - 2.0 * clamped)
95
+ }
96
+ puts "declared: #{smoothstep}"
97
+
98
+ # It keeps its block, so what the kernel runs and what Ruby computes can be
99
+ # put side by side. They agree to the last bit; that is the whole claim of
100
+ # this compiler, and here it is checkable rather than argued.
101
+ [-0.5, 0.25, 0.5, 2.0].each do |v|
102
+ puts " smoothstep(%5s) C %.17g Ruby %.17g" %
103
+ [v, smoothstep.call(v), smoothstep.block.call(v)]
104
+ end
105
+ puts
106
+
107
+ # And it is called from a kernel like any other, which is what gives kernels
108
+ # something they did not have: a body you can factor and name.
109
+ w = CArray.double(6).seq!(-0.2, 0.3)
110
+ blended = CArray.double(6)
111
+ low = CArray.double(6) { 10.0 }
112
+ high = CArray.double(6) { 20.0 }
113
+ CArray.jit_each {
114
+ blended = low + (high - low) * smoothstep.call(w)
115
+ }
116
+ puts "blend across a ramp"
117
+ puts " #{blended.to_a.map { |v| v.round(4) }.inspect}"
118
+ puts
119
+
120
+ # Calling itself
121
+ # --------------
122
+ # A declaration that gives a name puts that name in scope inside its own body,
123
+ # which is what C does, so the function can recurse. The spelling is `.call`,
124
+ # the one every C function takes here, and that is not a compromise: it keeps
125
+ # the block runnable, so the compiled recursion and the Ruby one can be put
126
+ # side by side.
127
+
128
+ fact = CArray.jit_function("double fact(double)") { |n|
129
+ n <= 1.0 ? 1.0 : n * fact.call(n - 1.0)
130
+ }
131
+ puts "5! compiled C #{fact.call(5.0)}"
132
+ puts " same block in Ruby #{fact.block.call(5.0)}"
133
+ puts fact.c_source.lines.grep(/carray_jit_fact/).map { |line| " #{line.strip}" }
134
+ puts " -- `fact` in the block is a spelling; the call goes to the qualified"
135
+ puts " symbol, so it cannot reach anything else named fact."
136
+ puts
137
+
138
+ # A pointer parameter is handed on the way C hands one on, so a recursion can
139
+ # walk an array.
140
+ total = CArray.jit_function("double total(int n, const double v[8])") { |n, v|
141
+ n == 0 ? 0.0 : v[n - 1] + total.call(n - 1, v)
142
+ }
143
+ values = CArray.double(8).seq!(1.0)
144
+ puts "sum of #{values.to_a.map(&:to_i).inspect} by recursion #{total.call(8, values)}"
145
+ puts " matches Ruby #{total.call(8, values) == total.block.call(8, values)}"
146
+ puts
147
+
148
+ # Dividing by zero
149
+ # ----------------
150
+ # `6 % 0` raises in Ruby, and a kernel raises it too -- it is handed somewhere
151
+ # to report. A compiled function has only the signature its declaration gave
152
+ # it, so the object carries a place of its own: one exported int the division
153
+ # helpers write into, declared only when the body can reach it. The C still
154
+ # returns a number and touches no Ruby value; `call` is what looks afterwards.
155
+
156
+ remainder = CArray.jit_function("int r(int a, int b)") { |a, b| a % b }
157
+ puts "7 % 3 C #{remainder.call(7, 3)} Ruby #{remainder.block.call(7, 3)}"
158
+ [[remainder, 7, 0]].each do |f, a, b|
159
+ compiled = begin; f.call(a, b); rescue => error; "#{error.class}: #{error.message}"; end
160
+ in_ruby = begin; f.block.call(a, b); rescue => error; "#{error.class}: #{error.message}"; end
161
+ puts "7 % 0 C #{compiled}"
162
+ puts " Ruby #{in_ruby}"
163
+ end
164
+ puts remainder.c_source.lines.grep(/carray_jit_error/).map { |line| " #{line.strip}" }
165
+
166
+ # A float division is not that case -- an infinity is the answer in Ruby, in C
167
+ # and here -- so a body that only divides floats declares no flag.
168
+ float_divide = CArray.jit_function("double d(double a, double b)") { |a, b| a / b }
169
+ puts "1.0 / 0.0 #{float_divide.call(1.0, 0.0)}, and no flag: " \
170
+ "#{!float_divide.c_source.include?("carray_jit_error")}"
171
+ puts
172
+
173
+ # Handing one back out
174
+ # --------------------
175
+ # The compiled object references no Ruby symbol at all, so the address is safe
176
+ # to call from a library that knows nothing about Ruby -- and gsl_function is
177
+ # `double (*)(double x, void *params)`, which is written here exactly as GSL's
178
+ # own documentation writes it. The body may not read the pointer: it is a
179
+ # slot the ABI requires, not a value.
180
+
181
+ integrand = CArray.jit_function("double (*)(double x, void *params)") { |x, params|
182
+ Math.exp(-x * x)
183
+ }
184
+ puts "for a gsl_function slot: #{integrand}"
185
+ puts " pointer: 0x#{integrand.pointer.to_i.to_s(16)}"
186
+ puts
187
+
188
+ # Coefficients from outside, without capturing them
189
+ # ------------------------------------------------
190
+ # A compiled function reaches nothing outside its parameters, so what it needs
191
+ # arrives as one -- which is what C does anyway, and C's declarator says the
192
+ # rest: `const` is the read/write distinction, and a length is a length.
193
+
194
+ poly = CArray.jit_function("double (*)(double x, const double coef[3])") { |x, c|
195
+ c[0] + c[1] * x + c[2] * x * x
196
+ }
197
+ coef = CArray.double(3) { |i| [1.0, 2.0, 3.0][i] }
198
+ puts "1 + 2x + 3x^2 at x = 2"
199
+ puts " compiled C #{poly.call(2.0, coef)}"
200
+ puts " same block in Ruby #{poly.block.call(2.0, coef)}"
201
+ puts " -- `coef[0]` means the same to a CArray as to a C pointer, so the"
202
+ puts " body is unchanged between them."
203
+ puts
204
+
205
+ # The whole ODE signature is sayable, with no half of it invented here.
206
+ ode = CArray.jit_function(
207
+ "int (*)(double t, const double y[2], double dydt[2], void *params)"
208
+ ) { |t, y, dydt, params|
209
+ dydt[0] = y[1]
210
+ dydt[1] = -y[0]
211
+ 0
212
+ }
213
+ state = CArray.double(2) { |i| [1.0, 0.0][i] }
214
+ derivative = CArray.double(2)
215
+ ode.call(0.0, state, derivative, nil)
216
+ puts "harmonic oscillator: y = #{state.to_a.inspect} -> dy/dt = #{derivative.to_a.inspect}"
217
+ puts ode.c_source.lines.last(6).join
218
+
219
+ # ...and a kernel can hand over one of its own arrays
220
+ # ---------------------------------------------------
221
+ # `x[i]` is a cell and `coef` is the whole array. Which is meant comes from
222
+ # the declaration rather than from the spelling.
223
+
224
+ ramp = CArray.double(6).seq!(0.0, 0.5)
225
+ fitted = CArray.double(6)
226
+ CArray.jit_for(6) { |i| fitted[i] = poly.call(ramp[i], coef) }
227
+ puts "1 + 2x + 3x^2 along a ramp"
228
+ puts " #{fitted.to_a.map { |v| v.round(4) }.inspect}"
229
+ puts " matches Ruby #{fitted.to_a == ramp.to_a.map { |v| poly.block.call(v, coef) }}"
230
+ puts
231
+
232
+ # What it costs
233
+ # -------------
234
+ # Against the two things a Ruby user can do today: call it through Fiddle one
235
+ # cell at a time, or find a Math method that happens to exist.
236
+
237
+ n = 200_000
238
+ big = CArray.double(n).seq!(1.0, 1e-5)
239
+ big_out = CArray.double(n)
240
+ gamma = CArray.jit_extern("double tgamma(double)")
241
+
242
+ def timed
243
+ 2.times { yield }
244
+ Benchmark.realtime { 3.times { yield } } / 3
245
+ end
246
+
247
+ compiled = timed { CArray.jit_each { big_out = gamma.call(big) } }
248
+ values = big.to_a
249
+ jit_for_fiddle = timed { values.map { |v| gamma.call(v) } }
250
+ ruby_math = timed { values.map { |v| Math.gamma(v) } }
251
+
252
+ puts "tgamma over #{n} cells"
253
+ puts " in the kernel %7.2f ms %7.1f ns/element" %
254
+ [compiled * 1e3, compiled / n * 1e9]
255
+ puts " Fiddle, per cell %7.2f ms %7.1f ns/element %.0fx" %
256
+ [jit_for_fiddle * 1e3, jit_for_fiddle / n * 1e9, jit_for_fiddle / compiled]
257
+ puts " Math.gamma map %7.2f ms %7.1f ns/element %.0fx" %
258
+ [ruby_math * 1e3, ruby_math / n * 1e9, ruby_math / compiled]
259
+ puts
260
+ puts "-- and Ruby has no Math method for j0 at all."