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