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,115 @@
|
|
|
1
|
+
# Steady heat on a plate: Laplace's equation by Jacobi relaxation.
|
|
2
|
+
#
|
|
3
|
+
# The plate's edges are held at fixed temperatures and the interior settles
|
|
4
|
+
# into the average of its neighbours. One sweep is a five-point stencil, and
|
|
5
|
+
# the boundary is the whole difficulty: those cells are data, not something to
|
|
6
|
+
# compute, so the sweep has to leave them exactly as it found them. That is
|
|
7
|
+
# `border: :skip`, said at the call -- the loop itself knows nothing about it.
|
|
8
|
+
#
|
|
9
|
+
# Beside it, the same sweep written over whole arrays, which is what this would
|
|
10
|
+
# be without a compiler: four shifted views added, four passes over the plate,
|
|
11
|
+
# and a fresh array for each.
|
|
12
|
+
#
|
|
13
|
+
# ruby examples/applications/relaxation.rb
|
|
14
|
+
|
|
15
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
16
|
+
require "carray/jit"
|
|
17
|
+
|
|
18
|
+
ROWS, COLUMNS = 120, 160
|
|
19
|
+
SWEEPS = 1200
|
|
20
|
+
|
|
21
|
+
def fresh_plate
|
|
22
|
+
plate = CArray.double(ROWS, COLUMNS)
|
|
23
|
+
plate[nil, 0] = 25.0 # the sides, somewhere in between
|
|
24
|
+
plate[nil, -1] = 25.0
|
|
25
|
+
plate[0, nil] = 100.0 # the top edge is hot, corners included
|
|
26
|
+
plate[-1, nil] = 0.0 # the bottom is held at zero
|
|
27
|
+
plate
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# One sweep. `into:` writes the array we already have, and gives it back;
|
|
31
|
+
# `border: :skip` leaves the four edges holding the temperatures they were set
|
|
32
|
+
# to, so nothing in the kernel has to know that they are the boundary.
|
|
33
|
+
def sweep (plate, scratch)
|
|
34
|
+
CArray.jit_stencil(plate, border: :skip, into: scratch) { |a|
|
|
35
|
+
0.25 * (a[-1, 0] + a[1, 0] + a[0, -1] + a[0, 1])
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
plate = fresh_plate
|
|
40
|
+
scratch = plate.copy
|
|
41
|
+
|
|
42
|
+
SWEEPS.times do
|
|
43
|
+
sweep(plate, scratch)
|
|
44
|
+
plate, scratch = scratch, plate
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
puts "the plate after #{SWEEPS} sweeps"
|
|
48
|
+
(0...12).map { |t| t * (ROWS - 1) / 11 }.each do |i|
|
|
49
|
+
row = (0...40).map { |t| t * (COLUMNS - 1) / 39 }.map { |j|
|
|
50
|
+
" .:-=+*#%@"[(plate[i, j] / 100.0 * 9).round]
|
|
51
|
+
}.join
|
|
52
|
+
puts " " + row
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The edges are still what they were set to, and the interior is the average
|
|
56
|
+
# of its neighbours to within the residual below.
|
|
57
|
+
edges_held = plate[0, nil].to_a.all?(100.0) && plate[-1, nil].to_a.all?(0.0)
|
|
58
|
+
residual = 0.0
|
|
59
|
+
(1...(ROWS-1)).each do |i|
|
|
60
|
+
(1...(COLUMNS-1)).each do |j|
|
|
61
|
+
average = 0.25 * (plate[i-1, j] + plate[i+1, j] + plate[i, j-1] + plate[i, j+1])
|
|
62
|
+
difference = (plate[i, j] - average).abs
|
|
63
|
+
residual = difference if difference > residual
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
puts format(" the edges were left alone %s", edges_held)
|
|
67
|
+
puts format(" largest residual %.2e", residual)
|
|
68
|
+
|
|
69
|
+
# The same sweep over whole arrays, for the answer to check against and the
|
|
70
|
+
# time to measure against. The interior is written from four shifted views;
|
|
71
|
+
# the boundary is not addressed, which is what :skip does at the call.
|
|
72
|
+
def fused_sweep (plate, scratch)
|
|
73
|
+
scratch[1..-2, 1..-2] = 0.25 * (plate[0..-3, 1..-2] + plate[2..-1, 1..-2] +
|
|
74
|
+
plate[1..-2, 0..-3] + plate[1..-2, 2..-1])
|
|
75
|
+
scratch
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
fused = fresh_plate
|
|
79
|
+
spare = fused.copy
|
|
80
|
+
SWEEPS.times do
|
|
81
|
+
fused_sweep(fused, spare)
|
|
82
|
+
fused, spare = spare, fused
|
|
83
|
+
end
|
|
84
|
+
puts format(" agrees with whole arrays %s (largest difference %.1e)",
|
|
85
|
+
fused.to_a == plate.to_a, (fused - plate).abs.max)
|
|
86
|
+
|
|
87
|
+
# What a sweep costs each way.
|
|
88
|
+
warm = fresh_plate
|
|
89
|
+
warm_scratch = warm.copy
|
|
90
|
+
sweep(warm, warm_scratch)
|
|
91
|
+
|
|
92
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
93
|
+
100.times { sweep(warm, warm_scratch) }
|
|
94
|
+
stencilled = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / 100
|
|
95
|
+
|
|
96
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
97
|
+
100.times { fused_sweep(warm, warm_scratch) }
|
|
98
|
+
whole = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / 100
|
|
99
|
+
|
|
100
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
101
|
+
3.times do
|
|
102
|
+
(1...(ROWS-1)).each do |i|
|
|
103
|
+
(1...(COLUMNS-1)).each do |j|
|
|
104
|
+
warm_scratch[i, j] = 0.25 * (warm[i-1, j] + warm[i+1, j] +
|
|
105
|
+
warm[i, j-1] + warm[i, j+1])
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
interpreted = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / 3
|
|
110
|
+
|
|
111
|
+
puts
|
|
112
|
+
puts format("a sweep over %d cells", ROWS * COLUMNS)
|
|
113
|
+
puts format(" jit_stencil %.3f ms", stencilled * 1e3)
|
|
114
|
+
puts format(" four shifted views %.3f ms (%.1fx)", whole * 1e3, whole / stencilled)
|
|
115
|
+
puts format(" the same loop in Ruby %.3f ms (%.0fx)", interpreted * 1e3, interpreted / stencilled)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Quality control on a sensor record with gaps
|
|
2
|
+
#
|
|
3
|
+
# Real measurements have holes in them, and CArray already has a mask to say
|
|
4
|
+
# where. What is awkward in a loop is that the rules refer to the holes: fill
|
|
5
|
+
# a gap of one sample from its neighbours, drop a spike, and take the daily
|
|
6
|
+
# mean only from the days that have enough readings left.
|
|
7
|
+
#
|
|
8
|
+
# `reading[i] == UNDEF` is how you ask in Ruby, and it means the same inside a
|
|
9
|
+
# kernel -- it reads the mask, not the value, so what the branch writes is not
|
|
10
|
+
# itself masked. That is what makes filling a hole possible at all.
|
|
11
|
+
#
|
|
12
|
+
# ruby examples/applications/sensor_gaps.rb
|
|
13
|
+
|
|
14
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
15
|
+
require "carray/jit"
|
|
16
|
+
|
|
17
|
+
days, hours = 30, 24
|
|
18
|
+
random = Random.new(20260902)
|
|
19
|
+
|
|
20
|
+
# Hourly temperature: a daily cycle, a seasonal drift, and noise.
|
|
21
|
+
reading = CArray.double(days, hours) { |d, h|
|
|
22
|
+
15.0 + 8.0 * Math.sin((h - 9) * Math::PI / 12) + d * 0.1 +
|
|
23
|
+
random.rand(-1.0..1.0)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# The logger dropped some samples, and once reported a spike.
|
|
27
|
+
dropped = 0
|
|
28
|
+
days.times do |d|
|
|
29
|
+
hours.times do |h|
|
|
30
|
+
if random.rand < 0.06
|
|
31
|
+
reading[d, h] = UNDEF
|
|
32
|
+
dropped += 1
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
reading[7, 13] = 71.4
|
|
37
|
+
reading[19, 3] = -40.0
|
|
38
|
+
|
|
39
|
+
# And on day 12 it was offline from midnight until noon.
|
|
40
|
+
(0..12).each { |h| reading[12, h] = UNDEF }
|
|
41
|
+
dropped = reading.count_masked
|
|
42
|
+
|
|
43
|
+
puts "#{days} days x #{hours} hours, #{dropped} samples missing"
|
|
44
|
+
|
|
45
|
+
# 1. Reject physically impossible readings. Writing UNDEF marks the cell
|
|
46
|
+
# missing and leaves its value alone.
|
|
47
|
+
rejected = CArray.int32(1)
|
|
48
|
+
CArray.jit_for(days, hours) { |d, h|
|
|
49
|
+
if reading[d, h] > 50.0
|
|
50
|
+
reading[d, h] = UNDEF
|
|
51
|
+
elsif reading[d, h] < -30.0
|
|
52
|
+
reading[d, h] = UNDEF
|
|
53
|
+
end
|
|
54
|
+
}
|
|
55
|
+
puts " #{reading.count_masked - dropped} readings rejected as out of range"
|
|
56
|
+
|
|
57
|
+
# 2. Fill a hole from its neighbours, but only where both neighbours are there.
|
|
58
|
+
# Where they are not, the average reads a masked cell and the result comes
|
|
59
|
+
# out masked by itself -- the rule does not have to be written twice.
|
|
60
|
+
filled = CArray.double(days, hours)
|
|
61
|
+
CArray.jit_for(days, 1...(hours-1)) { |d, h|
|
|
62
|
+
if reading[d, h] == UNDEF
|
|
63
|
+
filled[d, h] = 0.5 * (reading[d, h-1] + reading[d, h+1])
|
|
64
|
+
else
|
|
65
|
+
filled[d, h] = reading[d, h]
|
|
66
|
+
end
|
|
67
|
+
}
|
|
68
|
+
filled[nil, 0] = reading[nil, 0]
|
|
69
|
+
filled[nil, hours-1] = reading[nil, hours-1]
|
|
70
|
+
|
|
71
|
+
recovered = reading.count_masked - filled.count_masked
|
|
72
|
+
puts " #{recovered} holes interpolated, #{filled.count_masked} still missing"
|
|
73
|
+
|
|
74
|
+
# 3. The daily mean, from the readings that survived -- and a day with fewer
|
|
75
|
+
# than twenty valid hours does not get a mean at all.
|
|
76
|
+
mean = CArray.double(days)
|
|
77
|
+
valid = CArray.int32(days)
|
|
78
|
+
CArray.jit_for(days) { |d|
|
|
79
|
+
total = 0.0
|
|
80
|
+
count = 0
|
|
81
|
+
(0...hours).each { |h|
|
|
82
|
+
if filled[d, h] == UNDEF
|
|
83
|
+
count = count
|
|
84
|
+
else
|
|
85
|
+
total = total + filled[d, h]
|
|
86
|
+
count = count + 1
|
|
87
|
+
end
|
|
88
|
+
}
|
|
89
|
+
valid[d] = count
|
|
90
|
+
if count >= 20
|
|
91
|
+
mean[d] = total / count
|
|
92
|
+
else
|
|
93
|
+
mean[d] = UNDEF
|
|
94
|
+
end
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
puts " #{mean.count_masked} of #{days} days rejected for too few readings"
|
|
98
|
+
puts
|
|
99
|
+
puts " day valid mean"
|
|
100
|
+
(0...days).each do |d|
|
|
101
|
+
next unless d < 6 || d > days - 4 || valid[d] < 20
|
|
102
|
+
text = mean[d] == UNDEF ? " -- " : format("%6.2f", mean[d])
|
|
103
|
+
bar = mean[d] == UNDEF ? "" : "#" * ((mean[d] - 10) * 2).round
|
|
104
|
+
puts format(" %3d %5d %s %s", d, valid[d], text, bar)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The same rules written as a Ruby loop, for comparison -- and to check that
|
|
108
|
+
# they agree, which is possible precisely because the kernel asks about the
|
|
109
|
+
# mask rather than relying on it propagating.
|
|
110
|
+
reference = Array.new(days) do |d|
|
|
111
|
+
values = (0...hours).map { |h| filled[d, h] == UNDEF ? nil : filled[d, h] }.compact
|
|
112
|
+
values.size >= 20 ? values.sum / values.size : nil
|
|
113
|
+
end
|
|
114
|
+
agrees = (0...days).all? { |d|
|
|
115
|
+
reference[d].nil? ? mean[d] == UNDEF : (mean[d] - reference[d]).abs < 1e-12
|
|
116
|
+
}
|
|
117
|
+
puts
|
|
118
|
+
puts " agrees with the same rules written in Ruby: #{agrees}"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# The sieve of Eratosthenes.
|
|
2
|
+
#
|
|
3
|
+
# Each prime crosses out its own multiples, and how many that is depends on
|
|
4
|
+
# the prime: the inner loop's length is different for every cell and is not
|
|
5
|
+
# knowable before the loop runs, which is what `while` is for. The cells it
|
|
6
|
+
# writes are at computed subscripts, and they are cells this same loop will
|
|
7
|
+
# later read -- crossing out 2's multiples is what makes 4 composite before
|
|
8
|
+
# the loop reaches it.
|
|
9
|
+
#
|
|
10
|
+
# Written over whole arrays this is a pass per prime, each one building an
|
|
11
|
+
# index array to scatter through. Written here it is the sieve as it is
|
|
12
|
+
# stated.
|
|
13
|
+
#
|
|
14
|
+
# ruby examples/applications/sieve.rb
|
|
15
|
+
|
|
16
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
17
|
+
require "carray/jit"
|
|
18
|
+
|
|
19
|
+
N = 1_000_000
|
|
20
|
+
|
|
21
|
+
def sieve (n)
|
|
22
|
+
flags = CArray.int8(n).fill(1)
|
|
23
|
+
flags[0] = 0
|
|
24
|
+
flags[1] = 0
|
|
25
|
+
CArray.jit_for(2...n) { |i|
|
|
26
|
+
if flags[i] == 1
|
|
27
|
+
j = i * i # everything smaller was crossed out already
|
|
28
|
+
while j < n
|
|
29
|
+
flags[j] = 0
|
|
30
|
+
j = j + i
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
}
|
|
34
|
+
flags
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
flags = sieve(N)
|
|
38
|
+
primes = flags.eq(1).count(1)
|
|
39
|
+
|
|
40
|
+
puts "primes below #{N}"
|
|
41
|
+
puts " how many #{primes}"
|
|
42
|
+
puts " the first few #{(0...30).select { |i| flags[i] == 1 }.inspect}"
|
|
43
|
+
puts " the largest #{(0...N).reverse_each.find { |i| flags[i] == 1 }}"
|
|
44
|
+
|
|
45
|
+
# The inner loop cannot be written as an extent here. `(i*i...n).step(i)` is
|
|
46
|
+
# refused, and so is any inner range whose bounds a cell works out: an inner
|
|
47
|
+
# loop runs over an integer expression in literals and captured scalars, so
|
|
48
|
+
# that the kernel knows its shape before it runs. `while` is the spelling
|
|
49
|
+
# that carries the bound the data decides.
|
|
50
|
+
begin
|
|
51
|
+
counts = CArray.int32(N)
|
|
52
|
+
CArray.jit_for(2...N) { |i|
|
|
53
|
+
seen = 0
|
|
54
|
+
(i...N).each { |j| seen = seen + 1 }
|
|
55
|
+
counts[i] = seen
|
|
56
|
+
}
|
|
57
|
+
rescue CArray::JIT::Unsupported => error
|
|
58
|
+
puts " a per-cell inner range refused: #{error.message.sub(/ \(at line.*/m, "")}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The same sieve in Ruby, for the answer and the time.
|
|
62
|
+
def sieve_in_ruby (n)
|
|
63
|
+
flags = Array.new(n, 1)
|
|
64
|
+
flags[0] = 0
|
|
65
|
+
flags[1] = 0
|
|
66
|
+
i = 2
|
|
67
|
+
while i < n
|
|
68
|
+
if flags[i] == 1
|
|
69
|
+
j = i * i
|
|
70
|
+
while j < n
|
|
71
|
+
flags[j] = 0
|
|
72
|
+
j = j + i
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
i = i + 1
|
|
76
|
+
end
|
|
77
|
+
flags
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
in_ruby = sieve_in_ruby(N)
|
|
81
|
+
puts " agrees with Ruby #{flags.to_a == in_ruby}"
|
|
82
|
+
|
|
83
|
+
sieve(N) # compiled and cached on the first call
|
|
84
|
+
|
|
85
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
86
|
+
5.times { sieve(N) }
|
|
87
|
+
compiled = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / 5
|
|
88
|
+
|
|
89
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
90
|
+
sieve_in_ruby(N)
|
|
91
|
+
interpreted = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
92
|
+
|
|
93
|
+
puts
|
|
94
|
+
puts format("one sieve to %d: %.1f ms compiled, %.1f ms in Ruby (%.0fx)",
|
|
95
|
+
N, compiled * 1e3, interpreted * 1e3, interpreted / compiled)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Edge detection on an image
|
|
2
|
+
#
|
|
3
|
+
# A Sobel filter is two 3x3 convolutions and a hypotenuse. With array
|
|
4
|
+
# operators that is eighteen shifted arrays, eighteen passes over the image and
|
|
5
|
+
# a dozen temporaries the size of it; here it is the filter as written, one
|
|
6
|
+
# pass, nothing allocated in between.
|
|
7
|
+
#
|
|
8
|
+
# ruby examples/applications/sobel_edges.rb
|
|
9
|
+
|
|
10
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
11
|
+
require "carray/jit"
|
|
12
|
+
|
|
13
|
+
def sobel (image, edges)
|
|
14
|
+
rows, columns = image.dim
|
|
15
|
+
CArray.jit_for(1...(rows-1), 1...(columns-1)) { |i, j|
|
|
16
|
+
gx = (image[i-1, j+1] + 2.0 * image[i, j+1] + image[i+1, j+1]) -
|
|
17
|
+
(image[i-1, j-1] + 2.0 * image[i, j-1] + image[i+1, j-1])
|
|
18
|
+
gy = (image[i+1, j-1] + 2.0 * image[i+1, j] + image[i+1, j+1]) -
|
|
19
|
+
(image[i-1, j-1] + 2.0 * image[i-1, j] + image[i-1, j+1])
|
|
20
|
+
edges[i, j] = Math.sqrt(gx * gx + gy * gy)
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# A synthetic picture: a disc, a bar, and a soft background gradient.
|
|
25
|
+
rows, columns = 40, 96
|
|
26
|
+
image = CArray.double(rows, columns)
|
|
27
|
+
rows.times do |i|
|
|
28
|
+
columns.times do |j|
|
|
29
|
+
y = (i - 20.0) / 16.0
|
|
30
|
+
x = (j - 30.0) / 16.0
|
|
31
|
+
value = 0.25 * (j.to_f / columns)
|
|
32
|
+
value += 0.7 if x * x + y * y < 1.0 # the disc
|
|
33
|
+
value += 0.6 if i.between?(8, 30) && j.between?(58, 84) # the bar
|
|
34
|
+
image[i, j] = value
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
edges = CArray.double(rows, columns)
|
|
39
|
+
sobel(image, edges)
|
|
40
|
+
|
|
41
|
+
def draw (field, title)
|
|
42
|
+
puts title
|
|
43
|
+
scale = field.max
|
|
44
|
+
field.to_a.each_slice(2) do |row, _|
|
|
45
|
+
puts " " + row.each_slice(1).map { |v|
|
|
46
|
+
" .:-=+*#@"[[(v.first / scale * 8).round, 8].min]
|
|
47
|
+
}.join
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
draw(image, "image")
|
|
52
|
+
draw(edges, "edges")
|
|
53
|
+
|
|
54
|
+
# On an image of a size you would actually filter -- the same filter, and the
|
|
55
|
+
# same loop written in Ruby, both over the whole thing.
|
|
56
|
+
size = 512
|
|
57
|
+
large = CArray.double(size, size) { |i, j| Math.sin(i * 0.03) * Math.cos(j * 0.02) }
|
|
58
|
+
result = CArray.double(size, size)
|
|
59
|
+
|
|
60
|
+
sobel(large, result) # compile it before timing it
|
|
61
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
62
|
+
5.times { sobel(large, result) }
|
|
63
|
+
compiled = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / 5
|
|
64
|
+
|
|
65
|
+
reference = CArray.double(size, size)
|
|
66
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
67
|
+
(1...(size-1)).each do |i|
|
|
68
|
+
(1...(size-1)).each do |j|
|
|
69
|
+
gx = (large[i-1, j+1] + 2.0 * large[i, j+1] + large[i+1, j+1]) -
|
|
70
|
+
(large[i-1, j-1] + 2.0 * large[i, j-1] + large[i+1, j-1])
|
|
71
|
+
gy = (large[i+1, j-1] + 2.0 * large[i+1, j] + large[i+1, j+1]) -
|
|
72
|
+
(large[i-1, j-1] + 2.0 * large[i-1, j] + large[i-1, j+1])
|
|
73
|
+
reference[i, j] = Math.sqrt(gx * gx + gy * gy)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
interpreted = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
77
|
+
|
|
78
|
+
puts format("%dx%d image: %.1f ms compiled, %.0f ms as a Ruby loop (%.0fx)",
|
|
79
|
+
size, size, compiled * 1e3, interpreted * 1e3, interpreted / compiled)
|
|
80
|
+
puts " identical results: #{result.to_a == reference.to_a}"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# An expression over whole arrays, in one pass.
|
|
2
|
+
#
|
|
3
|
+
# jit_each is jit_for's sibling, for work that reaches no neighbour: no
|
|
4
|
+
# index to name, so no extent to give, and the arrays are written the way
|
|
5
|
+
# CArray already spells the whole of one. What changes is not the expression
|
|
6
|
+
# but how it runs -- at the cell, reading the data once, instead of one pass
|
|
7
|
+
# per operation with intermediate arrays in between.
|
|
8
|
+
#
|
|
9
|
+
# Every name in the block is a cell, the loop being this compiler's and not
|
|
10
|
+
# written here, so the assignment is Ruby's own: `out = ...` writes the array
|
|
11
|
+
# `out` names outside. jit_map is the same block with its value asked for.
|
|
12
|
+
|
|
13
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
14
|
+
require "carray/jit"
|
|
15
|
+
|
|
16
|
+
n = 100_000
|
|
17
|
+
a = CArray.double(n).seq!(1.0)
|
|
18
|
+
b = CArray.double(n).seq!(0.5, 0.5)
|
|
19
|
+
c = CArray.double(n).seq!(2.0, -0.25)
|
|
20
|
+
out = CArray.double(n)
|
|
21
|
+
|
|
22
|
+
CArray.jit_each { out = (a + b) * (c - a) + b * c - a }
|
|
23
|
+
|
|
24
|
+
# The same expression evaluated by CArray, which allocates an array per
|
|
25
|
+
# operation along the way.
|
|
26
|
+
expected = (a + b) * (c - a) + b * c - a
|
|
27
|
+
|
|
28
|
+
puts "element-wise"
|
|
29
|
+
puts " first three cells #{out[0..2].to_a.inspect}"
|
|
30
|
+
puts " matches CArray #{out.to_a == expected.to_a}"
|
|
31
|
+
|
|
32
|
+
# The rank comes from the arrays rather than from the block, so the same
|
|
33
|
+
# expression serves whatever shape it is given, and broadcasting costs nothing
|
|
34
|
+
# -- a stretched axis arrives as a stride of zero.
|
|
35
|
+
matrix = CArray.double(3, 4).seq!(1.0)
|
|
36
|
+
row = CArray.double(1, 4).seq!(10.0, 10.0)
|
|
37
|
+
scaled = CArray.double(3, 4)
|
|
38
|
+
|
|
39
|
+
CArray.jit_each { scaled = matrix * row }
|
|
40
|
+
|
|
41
|
+
puts " broadcast row #{scaled.to_a.inspect}"
|
|
42
|
+
puts " matches CArray #{scaled.to_a == (matrix * row).to_a}"
|
|
43
|
+
|
|
44
|
+
# jit_map: the same block, with its value asked for.
|
|
45
|
+
#
|
|
46
|
+
# The last statement is the value every cell of the result gets, and the
|
|
47
|
+
# result is allocated here -- typed from that value rather than from the
|
|
48
|
+
# arrays -- so there is no output array to name. The name is what says a
|
|
49
|
+
# value comes back; the block is read exactly the same way.
|
|
50
|
+
|
|
51
|
+
sums = CArray.jit_map { a + b }
|
|
52
|
+
puts
|
|
53
|
+
puts "asking for the value back"
|
|
54
|
+
puts " first three cells #{sums[0..2].to_a.inspect}"
|
|
55
|
+
puts " matches CArray #{sums.to_a == (a + b).to_a}"
|
|
56
|
+
|
|
57
|
+
# It is the same shape of block CArray.jit_function takes and not the same thing:
|
|
58
|
+
# there the loop belongs to whoever calls the function, so the body may close
|
|
59
|
+
# over nothing. Here the loop is ours and the body is inlined into it.
|
|
60
|
+
weight = 0.25
|
|
61
|
+
blended = CArray.jit_map { a * weight + b * (1.0 - weight) }
|
|
62
|
+
puts " closing over a value #{blended[0..2].to_a.inspect}"
|
|
63
|
+
|
|
64
|
+
# An assignment is a statement with a value -- Ruby's rule, not a special
|
|
65
|
+
# case here -- so a block may write an array of yours and hand the same value
|
|
66
|
+
# back. Which is jit_each's block, asked a different question.
|
|
67
|
+
kept = CArray.double(n)
|
|
68
|
+
also = CArray.jit_map { kept = a - b }
|
|
69
|
+
puts " written and returned #{kept[0..2].to_a == also[0..2].to_a}"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# A stencil: each cell from its neighbours.
|
|
2
|
+
#
|
|
3
|
+
# Naming the indices is what lets a kernel reach a neighbouring cell, and the
|
|
4
|
+
# extents say which cells are written -- here the interior, leaving the border
|
|
5
|
+
# alone. The offsets mean what they mean in Ruby: src[i-1, j] is the cell at
|
|
6
|
+
# index i-1.
|
|
7
|
+
|
|
8
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
9
|
+
require "carray/jit"
|
|
10
|
+
|
|
11
|
+
rows, columns = 6, 8
|
|
12
|
+
src = CArray.double(rows, columns).seq!(1.0)
|
|
13
|
+
out = CArray.double(rows, columns)
|
|
14
|
+
|
|
15
|
+
CArray.jit_for(1...(rows-1), 1...(columns-1)) { |i, j|
|
|
16
|
+
out[i, j] = 0.25 * (src[i-1, j] + src[i+1, j] + src[i, j-1] + src[i, j+1])
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
reference = CArray.double(rows, columns)
|
|
20
|
+
(1...(rows-1)).each do |i|
|
|
21
|
+
(1...(columns-1)).each do |j|
|
|
22
|
+
reference[i, j] = 0.25 * (src[i-1, j] + src[i+1, j] + src[i, j-1] + src[i, j+1])
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
puts "five-point stencil"
|
|
27
|
+
puts " interior matches the Ruby loop #{out.to_a == reference.to_a}"
|
|
28
|
+
puts " border untouched #{out[0, nil].to_a.all?(0.0)}"
|
|
29
|
+
|
|
30
|
+
# An extent may step by more than one, which changes which cells are written
|
|
31
|
+
# and therefore which offsets are dependencies: with a step of two, a[i-1] is
|
|
32
|
+
# a cell this loop never writes.
|
|
33
|
+
values = CArray.double(10).seq!(1.0)
|
|
34
|
+
CArray.jit_for((2...10).step(2)) { |i| values[i] = values[i-1] * 100.0 }
|
|
35
|
+
puts " every other cell #{values.to_a.inspect}"
|
|
36
|
+
|
|
37
|
+
# Written this way the border is what the extents avoid, and the cells there
|
|
38
|
+
# keep whatever the output array held -- zeros, which cannot be told from
|
|
39
|
+
# zeros that were computed. The same stencil with the loop implied and the
|
|
40
|
+
# edge said at the call is jit_stencil's; see 14_stencil_window.rb.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# A recurrence: each cell from the ones before it.
|
|
2
|
+
#
|
|
3
|
+
# This is the case a vectorised library cannot help with, because cell i is
|
|
4
|
+
# not available until cell i-1 has been written. Reading behind the cell
|
|
5
|
+
# being written propagates only upward, and the extent is where that is
|
|
6
|
+
# written down -- by the caller, since nothing else can know it was meant.
|
|
7
|
+
|
|
8
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
9
|
+
require "carray/jit"
|
|
10
|
+
|
|
11
|
+
# Legendre polynomials at x, by their three-term recurrence.
|
|
12
|
+
n = 24
|
|
13
|
+
x = 0.5
|
|
14
|
+
legendre = CArray.double(n)
|
|
15
|
+
legendre[0] = 1.0
|
|
16
|
+
legendre[1] = x
|
|
17
|
+
|
|
18
|
+
CArray.jit_for(2...n) { |i|
|
|
19
|
+
w = x * legendre[i-1]
|
|
20
|
+
wy = w - legendre[i-2]
|
|
21
|
+
legendre[i] = wy + w - wy / i
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
reference = Array.new(n, 0.0)
|
|
25
|
+
reference[0] = 1.0
|
|
26
|
+
reference[1] = x
|
|
27
|
+
(2...n).each do |i|
|
|
28
|
+
w = x * reference[i-1]
|
|
29
|
+
wy = w - reference[i-2]
|
|
30
|
+
reference[i] = wy + w - wy / i
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
puts "Legendre recurrence, x = #{x}"
|
|
34
|
+
puts " P_2 .. P_5 #{legendre[2..5].to_a.inspect}"
|
|
35
|
+
puts " bit-exact vs Ruby #{legendre.to_a == reference}"
|
|
36
|
+
|
|
37
|
+
# `wy / i` divides a Float by an Integer, so it is a Float division here as it
|
|
38
|
+
# is in Ruby. Where both sides are integers, the division is floored -- also
|
|
39
|
+
# as in Ruby, and not as C would truncate it.
|
|
40
|
+
counts = CArray.int32(6)
|
|
41
|
+
CArray.jit_for(6) { |i| counts[i] = (i - 4) / 3 }
|
|
42
|
+
puts " floored integer / #{counts.to_a.inspect} == #{(0...6).map { |i| (i - 4) / 3 }.inspect}"
|
|
43
|
+
|
|
44
|
+
# A range that would read outside the array is refused before anything runs,
|
|
45
|
+
# rather than quietly starting one cell later.
|
|
46
|
+
begin
|
|
47
|
+
CArray.jit_for(0...n) { |i| legendre[i] = legendre[i-1] * 2.0 }
|
|
48
|
+
rescue CArray::JIT::Unsupported => error
|
|
49
|
+
puts " refused: #{error.message.lines.first.strip}"
|
|
50
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# The Thomas algorithm: two sweeps in opposite directions.
|
|
2
|
+
#
|
|
3
|
+
# The back substitution reads x[i+1] -- ahead of the cell it writes -- so it
|
|
4
|
+
# has to run downward to propagate. `(n-2).step(0, -1)` is that, and is the
|
|
5
|
+
# spelling Ruby itself iterates backwards with; `(n-2)..0` is refused, because
|
|
6
|
+
# Ruby gives that Range no elements and the same loop written by hand would do
|
|
7
|
+
# nothing.
|
|
8
|
+
#
|
|
9
|
+
# The forward sweep also shows why a kernel writes as many arrays as it likes:
|
|
10
|
+
# cc and dd share a denominator, and splitting them in two would compute it
|
|
11
|
+
# twice.
|
|
12
|
+
|
|
13
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
14
|
+
require "carray/jit"
|
|
15
|
+
|
|
16
|
+
n = 2_000
|
|
17
|
+
random = Random.new(20260902)
|
|
18
|
+
|
|
19
|
+
# A diagonally dominant tridiagonal system: a is the subdiagonal, b the
|
|
20
|
+
# diagonal, c the superdiagonal, d the right-hand side.
|
|
21
|
+
a = CArray.double(n) { random.rand(-1.0..1.0) }
|
|
22
|
+
c = CArray.double(n) { random.rand(-1.0..1.0) }
|
|
23
|
+
b = CArray.double(n) { |i| 4.0 + random.rand }
|
|
24
|
+
d = CArray.double(n) { random.rand(-1.0..1.0) }
|
|
25
|
+
a[0] = 0.0
|
|
26
|
+
c[n-1] = 0.0
|
|
27
|
+
|
|
28
|
+
cc = CArray.double(n)
|
|
29
|
+
dd = CArray.double(n)
|
|
30
|
+
x = CArray.double(n)
|
|
31
|
+
|
|
32
|
+
cc[0] = c[0] / b[0]
|
|
33
|
+
dd[0] = d[0] / b[0]
|
|
34
|
+
|
|
35
|
+
CArray.jit_for(1...n) { |i| # upward: reads cc[i-1]
|
|
36
|
+
denominator = b[i] - a[i] * cc[i-1]
|
|
37
|
+
cc[i] = c[i] / denominator
|
|
38
|
+
dd[i] = (d[i] - a[i] * dd[i-1]) / denominator
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
x[n-1] = dd[n-1]
|
|
42
|
+
CArray.jit_for((n-2).step(0, -1)) { |i| # downward: reads x[i+1]
|
|
43
|
+
x[i] = dd[i] - cc[i] * x[i+1]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Multiply the solution back through the original system and look at what is
|
|
47
|
+
# left over.
|
|
48
|
+
residual = 0.0
|
|
49
|
+
n.times do |i|
|
|
50
|
+
row = b[i] * x[i]
|
|
51
|
+
row += a[i] * x[i-1] if i > 0
|
|
52
|
+
row += c[i] * x[i+1] if i < n - 1
|
|
53
|
+
residual = [residual, (row - d[i]).abs].max
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
puts "Thomas algorithm, n = #{n}"
|
|
57
|
+
puts " x[0..2] #{x[0..2].to_a.inspect}"
|
|
58
|
+
puts " max |A x - d| #{format('%.3e', residual)}"
|
|
59
|
+
|
|
60
|
+
# The extent is where the algorithm is stated, and nothing second-guesses it.
|
|
61
|
+
# Run the same body upward and it computes something else -- not an error, but
|
|
62
|
+
# not back substitution either: each cell reads an x[i+1] the sweep has not
|
|
63
|
+
# reached yet. It is the answer the same Ruby loop gives, which is the only
|
|
64
|
+
# thing this promises.
|
|
65
|
+
wrong_way = CArray.double(n)
|
|
66
|
+
wrong_way[n-1] = dd[n-1]
|
|
67
|
+
CArray.jit_for(0...(n-1)) { |i| wrong_way[i] = dd[i] - cc[i] * wrong_way[i+1] }
|
|
68
|
+
|
|
69
|
+
in_ruby = Array.new(n) { |i| i == n-1 ? dd[n-1] : 0.0 }
|
|
70
|
+
(0...(n-1)).each { |i| in_ruby[i] = dd[i] - cc[i] * in_ruby[i+1] }
|
|
71
|
+
|
|
72
|
+
wrong_residual = 0.0
|
|
73
|
+
n.times do |i|
|
|
74
|
+
row = b[i] * wrong_way[i]
|
|
75
|
+
row += a[i] * wrong_way[i-1] if i > 0
|
|
76
|
+
row += c[i] * wrong_way[i+1] if i < n - 1
|
|
77
|
+
wrong_residual = [wrong_residual, (row - d[i]).abs].max
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
puts " same body, upward max |A x - d| = #{format('%.3e', wrong_residual)}"
|
|
81
|
+
puts " ...which is what Ruby's own loop gives: #{wrong_way.to_a == in_ruby}"
|