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,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."
|