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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 255cd246bcdd06c49b0afd2aeaaa4c3086eb8b9c84f4ad04319129f7415c182f
4
+ data.tar.gz: 80fedb1eb6cfcfece4009d69d26251a58a2caa9d5975d1895bb34b9a7fa18ac7
5
+ SHA512:
6
+ metadata.gz: fa33123ca9012275607181b6808e2497da5e8d1908764bff23200299e9c972dd61e9b9e4d3ab58345b2fbcd5badba39ddd03b214ad9ccadde6413b37a32f3215
7
+ data.tar.gz: 45050b058cba426afc373e50f47797e4720968c1e4a26862322364d44a81a4610a991622b0e35d3d20519fb583e654558728a85393064978c9adfc23db5d9f06
data/.yardopts ADDED
@@ -0,0 +1,10 @@
1
+ --markup markdown
2
+ --output-dir .yard-html
3
+ --markup-provider redcarpet
4
+ --no-private
5
+ --readme README.md
6
+ --files CHANGELOG.md
7
+ lib/carray/jit.rb
8
+ lib/carray/jit/errors.rb
9
+ lib/carray/jit/kernel.rb
10
+ lib/carray/jit/c_function.rb
data/CHANGELOG.md ADDED
@@ -0,0 +1,84 @@
1
+ # Changelog
2
+
3
+ Releases are recorded here from 0.1.0, which is the first.
4
+
5
+ <!-- Newest first, at both levels: a new release section goes above the
6
+ ones below it, and a new entry goes directly under its own release
7
+ heading -- not at the end of the section. The kind of change is
8
+ carried by the `- Fix:` / `- Change:` / `- New:` that opens the
9
+ entry; there are no per-kind subheadings.
10
+
11
+ An entry says three things and stops: what changed, what to do about
12
+ it (the migration, the replacement, the condition under which nothing
13
+ changes), and what is excluded. It does not say how the code was
14
+ broken, name the internals that were fixed, break down where the
15
+ speed came from, or argue the design -- those belong in the commit
16
+ message. Two to six lines.
17
+
18
+ The version here is this gem's own and is not CArray's. Which CArray
19
+ a release needs is said in the gemspec, and an entry says so only
20
+ when the answer changes. -->
21
+
22
+ ## 0.1.0
23
+
24
+ - New: `CArray.jit_for`, `CArray.jit_each` and `CArray.jit_map` compile their
25
+ block rather than running it. The block is read with Prism, translated to C
26
+ if it falls inside the recognized subset, compiled with the system C
27
+ compiler and called through Fiddle. `jit_for` takes the loop extents and
28
+ names the indices, so a cell may reach the ones around it -- a recurrence, a
29
+ stencil written out; `jit_each` writes into arrays of yours and `jit_map`
30
+ hands the value back, both at the cell with no index named. CArray 3.0.1
31
+ defines these three names and raises there; installing this gem is what
32
+ makes them compile.
33
+
34
+ - New: a block outside the subset raises `CArray::JIT::Unsupported`, naming
35
+ the construct and where it is, rather than falling back to a Ruby loop.
36
+ Nobody calls these methods except to make a per-cell computation fast, so
37
+ quietly doing the slow thing would answer a question that was not asked.
38
+ What the subset holds is in
39
+ [docs/03_SupportedFeatures.md](docs/03_SupportedFeatures.md#the-recognized-subset).
40
+
41
+ - New: every operation in a kernel means what Ruby means by it -- integer
42
+ division floors, `%` is not `fmod`, a Complex divides by Smith's method in
43
+ the order `complex.c` writes it. The exception is the order a reduction
44
+ takes its terms in: an accumulator is split into partial sums, which is
45
+ faster and usually the more accurate answer. `reassociate: false` on
46
+ `jit_for`, `CArray::JIT.reassociate = false`, or `CARRAY_JIT_REASSOCIATE=0`
47
+ for a whole process, asks for the serial order, and then the kernel agrees
48
+ with the Ruby loop bit for bit. `**` on a Complex is the one documented
49
+ exception.
50
+
51
+ - New: `CArray.jit_stencil` runs a block over windows onto its arrays, with
52
+ `border:` saying what happens at the edge -- `:mask` by default, because
53
+ CArray can say "not computed" and a border of zeros cannot be told from
54
+ zeros that were computed. `CArray.jit_contract` runs a contraction over a
55
+ repeated index, summing over the indices that do not appear on the left.
56
+
57
+ - New: `CArray.jit_extern` names a C function someone else compiled by quoting
58
+ its declaration, and `CArray.jit_function` compiles a body of your own. Both
59
+ hand back an object a kernel calls by address rather than per cell through
60
+ Fiddle, and either can be called from Ruby too.
61
+
62
+ - New: `CArray.fuse` is compiled where this gem is installed, without asking
63
+ for it and without changing what it computes. The gem registers an
64
+ expression evaluator with CArray at load; a program that only writes
65
+ `CArray.fuse { ... }` gets the same answer with or without it.
66
+
67
+ - New: compiled kernels are cached on disk, in `~/.cache/carray-jit` unless
68
+ `XDG_CACHE_HOME` or `CARRAY_JIT_CACHE` says otherwise, so a kernel is
69
+ compiled once rather than once per run. Entries are kept apart by this gem's
70
+ version, the CArray version they were built against, and the architecture:
71
+ a kernel is handed CArray's memory, on layouts CArray decides, so one
72
+ compiled against another version is rebuilt rather than reused.
73
+ `CARRAY_JIT_NO_CACHE` keeps the cache in a temporary directory that goes
74
+ away with the process, and `CARRAY_JIT_CC` names a different compiler.
75
+
76
+ - New: the `carray-jit` command reports and looks after that cache --
77
+ `status`, `list`, `show`, `clear`. It loads the compiler and nothing else,
78
+ so a cache can be inspected or cleared when CArray itself will not load.
79
+
80
+ - New: needs CArray 3.0.1 or later in the 3.0 series, and Ruby 3.2 or later.
81
+ The floor is where `ca_call_cslab_N`, the expression evaluator hook,
82
+ `__kernel_body__` and `BUILD_FLAGS` arrive. The ceiling is the next minor
83
+ because a kernel reaches CArray's C by address and pastes its kernel bodies
84
+ into generated C, neither of which the frozen author surface covers.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 himotoyoshi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # carray-jit
2
+
3
+ JIT compilation of CArray kernels written in Ruby.
4
+
5
+ This library was written to make explicit, cell-by-cell work on CArray arrays fast. A subset of Ruby chosen for numerical computation is JIT-compiled and evaluated as a C-level loop over CArray arrays: a cell that reads its neighbours, a recurrence, a loop written out. The aim is to combine that with CArray's already fast vectorized arithmetic and reductions, and so speed up numerical computation with CArray as a whole.
6
+
7
+ The block is read with Prism, translated to C if it falls inside that subset, compiled with the system C compiler and called through Fiddle. The compiled object is cached on disk, so a kernel is compiled once.
8
+
9
+ ## Status
10
+
11
+ 0.1.0 is the first release, and it still moves: behaviour can change between releases — see [CHANGELOG.md](CHANGELOG.md). A companion gem to CArray, it follows CArray's surface, which is not settled until CArray 3.1.
12
+
13
+ ## Features
14
+
15
+ - **A JIT compiler for C-level loops.** A block becomes one C function over CArray's own memory, built by the system C compiler and called through Fiddle.
16
+ - **Ordinary Ruby, and enough of it.** The source is parsed with Prism -- no DSL, no `eval` -- and every operation means what Ruby means by it, apart from the order a reduction takes its terms in. The subset is enough to state a numerical algorithm; what falls outside it is refused by name and line, not run as a Ruby loop.
17
+ - **A method for each shape.** `jit_for` for recurrences and loops written out, `jit_stencil` for windows at any rank, `CArray.jit_contract` for contraction over a repeated index, `jit_each` and `jit_map` for a pass that reaches no neighbour.
18
+ - **View- and mask-aware.** Columns, transposes and slices of slices are written in place without a copy, and masks propagate as CArray propagates them.
19
+ - **Pure C functions, in and out.** `jit_extern` binds one from a library and a kernel calls it by address; `jit_function` compiles one from a block and hands back a C function pointer.
20
+ - **The backend for `CArray.fuse`.** An array expression compiles instead of being walked a node at a time, without being asked and without changing the answer.
21
+ - **Compiled once, across processes.** The shared object is cached on disk and reused by later runs.
22
+
23
+ ## Install
24
+
25
+ ```
26
+ gem install carray-jit
27
+ ```
28
+
29
+ Or add it to your `Gemfile`:
30
+
31
+ ```ruby
32
+ gem "carray-jit"
33
+ ```
34
+
35
+ Requires:
36
+
37
+ - Ruby >= 3.2
38
+ - CArray >= 3.0.1, < 3.1
39
+ - A C compiler
40
+ - Prism and Fiddle (both ship with Ruby; Fiddle is a bundled gem)
41
+
42
+ ## Example
43
+
44
+ ```ruby
45
+ # Legendre polynomials at x = 0.5, by the recurrence that defines them.
46
+ # No array expression states this: P[i] needs P[i-1], which the same
47
+ # loop has just written.
48
+
49
+ x = 0.5
50
+ legendre = CArray.double(24)
51
+ legendre[0] = 1.0 # P_0(x) = 1
52
+ legendre[1] = x # P_1(x) = x
53
+
54
+ CArray.jit_for(2...24) { |i| # i is the loop index: 2, 3, ... 23
55
+ w = x * legendre[i-1] # a block local; its type is inferred
56
+ wy = w - legendre[i-2] # reaching back two cells
57
+ legendre[i] = wy + w - wy/i # the cell this pass writes
58
+ }
59
+
60
+ legendre[0..5].to_a
61
+ # => [1.0, 0.5, -0.125, -0.4375, -0.2890625, 0.08984375]
62
+ ```
63
+
64
+ `CArray.jit_for`, `CArray.jit_each` and `CArray.jit_map` are CArray's own names: without this gem they raise and point at `CArray.fuse`, which computes an array expression without a compiler. This gem is the compiler. An expression over whole arrays wants `CArray.fuse` and not these, and gets compiled anyway where this gem is installed.
65
+
66
+ ## Documentation
67
+
68
+ * [Introduction](docs/00_Introduction.md) — what carray-jit is: the gap it fills beside CArray, the subset a block is written in, and where a kernel gets its data
69
+ * [Getting started](docs/01_GettingStarted.md) — the block, its extents, what the three methods return, and where a kernel stands beside `a + b * c` and `CArray.fuse`
70
+ * [The shapes a kernel takes](docs/02_KernelShapes.md) — work that reaches no neighbour, extents and subscripts, stencils, reductions, and contraction over a repeated index
71
+ * [Supported features](docs/03_SupportedFeatures.md) — locals and types, branches, raising, the types that are not just a number, calling C, and the recognized subset with what it refuses
72
+ * [Compiling, caching and inspecting](docs/04_Compiling.md) — what the first call costs, where kernels are kept, reading the generated C, the `carray-jit` command, and what the suite checks
73
+ * [Design notes](docs/05_DesignNotes.md) — decisions that were not obvious, and why
74
+ * [Cheatsheet](docs/06_Cheatsheet.md) — the seven `jit_` methods and `CArray.fuse` on one page, to look up rather than to read
75
+
76
+ ## Contributing
77
+
78
+ Bug reports and feature requests are welcome — please open an issue.
79
+
80
+ **Before opening a pull request, read [CONTRIBUTING.md](CONTRIBUTING.md).** It is short, and it says which form a contribution is best sent in. A small, self-contained bug fix is fine as a pull request. Anything larger is better started as an issue: code here gets rewritten as a matter of course, so a patch for a larger change is likely to end up reimplemented rather than merged, and describing the problem gets you further than writing one.
81
+
82
+ ## Credits
83
+
84
+ carray-jit was designed and reviewed by a human developer; the implementation was produced in collaboration with AI coding tools.
85
+
86
+ ## License
87
+
88
+ MIT
data/bin/carray-jit ADDED
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Cache management for carray-jit.
5
+ #
6
+ # Loads only what it needs -- the compiler and its cache -- so that inspecting
7
+ # or clearing the cache works without CArray or the compiled extension being
8
+ # loadable.
9
+
10
+ require "optparse"
11
+
12
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
13
+
14
+ require "carray/jit/version"
15
+ require "carray/jit/errors"
16
+ require "carray/jit/compiler"
17
+
18
+ module CArrayJITCommand
19
+
20
+ Compiler = CArray::JIT::Compiler
21
+
22
+ BANNER = <<~TEXT
23
+ Usage: carray-jit <command> [options]
24
+
25
+ Commands:
26
+ status where the cache is and how big it is (default)
27
+ list the kernels cached for this environment
28
+ show <prefix> the C source of one cached kernel
29
+ clear remove this environment's kernels
30
+ clear --all remove every environment's kernels
31
+
32
+ Options:
33
+ TEXT
34
+
35
+ class << self
36
+
37
+ def run (argv)
38
+ options = { :all => false }
39
+ parser = OptionParser.new do |option|
40
+ option.banner = BANNER
41
+ option.on("--all", "clear every version and architecture") {
42
+ options[:all] = true
43
+ }
44
+ option.on("-v", "--version", "print the carray-jit version") do
45
+ puts CArray::JIT::VERSION
46
+ return 0
47
+ end
48
+ option.on("-h", "--help", "print this message") do
49
+ puts option
50
+ return 0
51
+ end
52
+ end
53
+ arguments = parser.parse(argv)
54
+
55
+ case arguments.shift
56
+ when nil, "status" then status
57
+ when "list" then list
58
+ when "show" then show(arguments.shift)
59
+ when "clear" then clear(options[:all])
60
+ else
61
+ warn(parser.to_s)
62
+ 1
63
+ end
64
+ rescue OptionParser::ParseError => error
65
+ warn("carray-jit: #{error.message}")
66
+ 1
67
+ rescue CArray::JIT::Error => error
68
+ warn("carray-jit: #{error.message}")
69
+ 1
70
+ end
71
+
72
+ private
73
+
74
+ def status
75
+ puts "root #{Compiler.cache_root}"
76
+ puts "environment #{File.basename(Compiler.cache_directory)}"
77
+ puts "kernels #{Compiler.entry_count}"
78
+ puts "size #{human(Compiler.byte_size)}"
79
+ puts "limit #{Compiler.entry_limit} kernels"
80
+
81
+ others = Compiler.stale_environments
82
+ unless others.empty?
83
+ puts
84
+ puts "other environments:"
85
+ others.sort.each do |path|
86
+ puts format(" %-32s %8s %s",
87
+ File.basename(path), human(directory_size(path)),
88
+ last_used(path))
89
+ end
90
+ end
91
+ 0
92
+ end
93
+
94
+ def list
95
+ entries = Compiler.entries.sort_by { |path| File.mtime(path) }.reverse
96
+ if entries.empty?
97
+ puts "no kernels cached for #{File.basename(Compiler.cache_directory)}"
98
+ return 0
99
+ end
100
+ entries.each do |path|
101
+ key = File.basename(path).sub(/\.[^.]+\z/, "")
102
+ puts format("%-12s %8s %s %s",
103
+ key[0, 12], human(File.size(path)),
104
+ File.mtime(path).strftime("%Y-%m-%d %H:%M"),
105
+ origin(path))
106
+ end
107
+ 0
108
+ end
109
+
110
+ def show (prefix)
111
+ unless prefix
112
+ warn("carray-jit: show needs the start of a kernel's key")
113
+ return 1
114
+ end
115
+ matches = Compiler.entries.select { |path|
116
+ File.basename(path).start_with?(prefix)
117
+ }
118
+ case matches.size
119
+ when 0
120
+ warn("carray-jit: no cached kernel starts with #{prefix}")
121
+ 1
122
+ when 1
123
+ source = source_path(matches.first)
124
+ unless File.exist?(source)
125
+ warn("carray-jit: the source for #{prefix} is missing")
126
+ return 1
127
+ end
128
+ puts File.read(source)
129
+ 0
130
+ else
131
+ warn("carray-jit: #{prefix} matches #{matches.size} kernels")
132
+ 1
133
+ end
134
+ end
135
+
136
+ def clear (everything)
137
+ removed = Compiler.clear(:everything => everything)
138
+ scope = everything ? "every environment" : File.basename(Compiler.cache_directory)
139
+ puts "removed #{removed} kernel#{'s' unless removed == 1} from #{scope}"
140
+ 0
141
+ end
142
+
143
+ # Where the kernel was written, which is what identifies it to whoever is
144
+ # looking: a hash names a kernel, a file and a line explain it. The
145
+ # generated signature is the fallback, for a kernel compiled from a
146
+ # `source:` string that has no file behind it.
147
+ def origin (object_path)
148
+ source = source_path(object_path)
149
+ return "(source missing)" unless File.exist?(source)
150
+ lines = File.readlines(source).first(20)
151
+ found = lines.find { |text| text =~ %r{\A \*\s+(\S+:\d+)\s*\z} }
152
+ return Regexp.last_match(1) if found
153
+ # A kernel compiled from a `source:` string has no file behind it, so
154
+ # the first line of the block stands in for one.
155
+ body = lines.find { |text| text.start_with?(" * ") }
156
+ return body[6..].strip[0, 60] if body
157
+ "(no origin recorded)"
158
+ rescue StandardError
159
+ "(unreadable)"
160
+ end
161
+
162
+ def source_path (object_path)
163
+ object_path.sub(/\.[^.]+\z/, ".c")
164
+ end
165
+
166
+ def directory_size (path)
167
+ Dir[File.join(path, "*")].sum { |entry|
168
+ File.file?(entry) ? File.size(entry) : 0
169
+ }
170
+ end
171
+
172
+ def last_used (path)
173
+ newest = Dir[File.join(path, "*")].map { |entry|
174
+ File.mtime(entry) rescue Time.at(0)
175
+ }.max
176
+ newest ? "last used #{newest.strftime('%Y-%m-%d')}" : "empty"
177
+ end
178
+
179
+ def human (bytes)
180
+ return "#{bytes} B" if bytes < 1024
181
+ units = ["KB", "MB", "GB"]
182
+ value = bytes.to_f
183
+ units.each do |unit|
184
+ value /= 1024
185
+ return format("%.1f %s", value, unit) if value < 1024
186
+ end
187
+ format("%.1f TB", value / 1024)
188
+ end
189
+
190
+ end
191
+
192
+ end
193
+
194
+ exit(CArrayJITCommand.run(ARGV))
@@ -0,0 +1,41 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "carray-jit"
3
+ spec.version = File.read(File.expand_path("lib/carray/jit/version.rb", __dir__))[/VERSION\s*=\s*"([^"]+)"/, 1]
4
+ spec.summary = "JIT compilation of CArray kernels written in Ruby"
5
+ spec.description = <<~TEXT
6
+ Write the loop in Ruby, and it runs at the speed of C -- one to two orders
7
+ of magnitude faster than interpreted. It is for what a CArray expression
8
+ cannot say: a cell that reaches its neighbours, a recurrence, a loop
9
+ written out. What may be in a block is a subset of Ruby, and a C compiler
10
+ has to be present at run time.
11
+ TEXT
12
+ spec.authors = ["himotoyoshi"]
13
+ spec.email = ["himotoyoshi@users.noreply.github.com"]
14
+ spec.license = "MIT"
15
+ spec.homepage = "https://github.com/himotoyoshi/carray-jit"
16
+
17
+ spec.required_ruby_version = ">= 3.2.0"
18
+
19
+ spec.files = Dir[
20
+ "lib/**/*.rb",
21
+ "ext/**/*.{c,h,rb}",
22
+ "bin/*",
23
+ "examples/**/*.rb",
24
+ "examples/README.md",
25
+ "README.md",
26
+ "CHANGELOG.md",
27
+ "docs/*.md",
28
+ "LICENSE",
29
+ "carray-jit.gemspec",
30
+ ".yardopts",
31
+ ]
32
+ spec.require_paths = ["lib"]
33
+ spec.bindir = "bin"
34
+ spec.executables = ["carray-jit"]
35
+ spec.extensions = ["ext/carray_jit_access/extconf.rb"]
36
+
37
+ # A kernel reaches CArray's C by address, so the ceiling is the next minor.
38
+ spec.add_dependency "carray", ">= 3.0.1", "< 3.1"
39
+ # fiddle ships as a bundled gem; depend on it explicitly so Ruby 3.5+ resolves it.
40
+ spec.add_dependency "fiddle"
41
+ end
@@ -0,0 +1,40 @@
1
+ # Introduction
2
+
3
+ carray-jit compiles a Ruby block into a C kernel that runs over CArray arrays. The block is not evaluated and is not a DSL: it is ordinary Ruby, read as source, translated to C, compiled by the system C compiler and called through Fiddle. What it computes is what the same block would have computed had Ruby run it, one cell at a time, at the speed of compiled C.
4
+
5
+ ## What CArray leaves out
6
+
7
+ CArray works on a whole array at once. Arithmetic and the mathematical functions apply to every element, reductions summarise along the axes you name, and sorts and scans walk the data in C. All of that is already fast, and nothing here replaces it.
8
+
9
+ What an array expression has no form for is a computation *between* elements. A cell that reads its neighbours. A cell that depends on the one computed before it. A loop whose length is not known until it runs. These are ordinary algorithms — a tridiagonal solve, a stencil with the boundary rule your problem actually has, a sieve, a recurrence from a paper — and until now writing one in Ruby meant a Ruby loop, and paying what the interpreter charges for every element.
10
+
11
+ This library is for that gap. The parts of an algorithm that have an array form stay with CArray, where they are fastest; the part that does not becomes a kernel. Together they run an algorithm end to end without dropping into a Ruby loop.
12
+
13
+ ## A subset, chosen rather than found
14
+
15
+ Not every Ruby block can become C. What may appear in one is a subset — arithmetic and comparison, locals, `if` and `while`, inner loops, subscripts at computed positions, the bitwise operators, the `Math` family, `raise` — and it was chosen deliberately, wide enough to state a numerical algorithm and narrow enough that each construct in it means in C exactly what Ruby means by it. Integer division floors. `%` is not `fmod`. A Complex divides by Smith's method in the order `complex.c` writes it.
16
+
17
+ A block that steps outside the subset is refused, by name and with a line number, rather than quietly running as a Ruby loop. Nobody reaches for a compiler except to make something fast, so falling back silently would answer a question that was not asked.
18
+
19
+ ## Where a kernel gets its data
20
+
21
+ A kernel reads and writes CArray arrays directly, in the memory CArray already holds them in. The arrays are the ones the block closes over, so nothing is named twice. Views are cells like any other — a column, a transpose, a slice of a slice — and are written in place without a copy. Masks propagate as CArray propagates them, and a kernel can ask whether a cell is missing.
22
+
23
+ A kernel can also call C. `jit_extern` binds a function from any library Fiddle can open, and the kernel calls it at its address rather than reaching it per cell through Fiddle; `jit_function` compiles one from a Ruby block of your own and hands back a pure C function pointer, which a kernel can call, Ruby can call, and a C library that knows nothing about either can be given.
24
+
25
+ Compiling costs something the first time and nothing afterwards: the shared object is cached on disk, keyed by the generated C and the compiler that built it, so a kernel is compiled once and reused by every later run.
26
+
27
+ ## Not only the blocks you write
28
+
29
+ Installing this gem also puts the compiler behind `CArray.fuse`. An array expression written there is compiled rather than walked a node at a time — without being asked for, and without changing the answer. An expression the compiler cannot address goes back to CArray, which walks it and arrives at the same result. So a program that never writes a kernel still gets something from having the gem installed.
30
+
31
+ ## How this guide is arranged
32
+
33
+ * [Getting started](01_GettingStarted.md) — the block, its extents, what the three methods return, and where a kernel stands beside `a + b * c` and `CArray.fuse`
34
+ * [The shapes a kernel takes](02_KernelShapes.md) — work that reaches no neighbour, extents and subscripts, stencils, reductions, and contraction over a repeated index
35
+ * [Supported features](03_SupportedFeatures.md) — locals and types, branches, raising, the types that are not just a number, calling C, and the recognized subset with what it refuses
36
+ * [Compiling, caching and inspecting](04_Compiling.md) — what the first call costs, where kernels are kept, reading the generated C, the `carray-jit` command, and what the suite checks
37
+ * [Design notes](05_DesignNotes.md) — decisions that were not obvious, and why
38
+ * [Cheatsheet](06_Cheatsheet.md) — the seven `jit_` methods and `CArray.fuse` on one page, to look up rather than to read
39
+
40
+ [examples/features/](../examples/features) is a tour of the same ground, one file per feature, and [examples/applications/](../examples/applications) holds small programs that use it to do something.
@@ -0,0 +1,80 @@
1
+ # Getting started
2
+
3
+ The block's parameters are the loop indices. Arrays and scalars are the variables it closes over -- `legendre` and `x` here -- so nothing is named twice, and the body reads like the Ruby loop it replaces.
4
+
5
+ An extent is a `Range`, an `Integer` standing for `0...n`, or an `Enumerator::ArithmeticSequence` -- what `(high - 1).step(low, -1)` returns -- one per index. A whole array is therefore `CArray.jit_for(*array.dim) { ... }`.
6
+
7
+ ```ruby
8
+ CArray.jit_for(1...(rows-1), 1...(columns-1)) { |i, j|
9
+ out[i, j] = 0.25 * (src[i-1, j] + src[i+1, j] + src[i, j-1] + src[i, j+1])
10
+ }
11
+ ```
12
+
13
+ `jit_for` returns the compiled kernel, whose `#c_source` is the C that ran. So does `jit_each`, the sibling method for work that reaches no neighbour and so needs neither an index nor an extent. `jit_map` is that same method with its value asked for, and returns the array of results instead.
14
+
15
+ [examples/applications/](../examples/applications) holds small programs that use this to do something -- a Game of Life, an implicit heat equation, edge detection, quality control on a sensor record -- each measured against the Ruby loop or the array expression it replaces. [examples/features/](../examples/features) is a tour of everything in these docs, one file per feature. `rake examples` runs them all.
16
+
17
+ ## Where the methods come from
18
+
19
+ `CArray.jit_for`, `CArray.jit_each` and `CArray.jit_map` are named by CArray, which defines them to raise: they say that they compile their block, that the compiler is this gem, and that it is not installed. Installing it replaces them with the ones that compile.
20
+
21
+ The name carries the rest. `jit_` says the block is read rather than run, and so has rules about what may be in it; a method called `per_cell` would not, and the subset would be something you found out about later. It also marks which methods need the compiler and which do not: an expression over whole arrays is `CArray.fuse`'s, and that one needs nothing installed.
22
+
23
+ The line between these two falls where the block's names fall -- a block that names indices is `jit_for`'s, a block that names none is `jit_each`'s -- and each turns the other's block away rather than quietly doing something with it.
24
+
25
+ ## No fallback
26
+
27
+ A block outside the compilable subset raises `CArray::JIT::Unsupported`, and so does one written with no compiler installed. Neither quietly runs the block as a Ruby loop instead.
28
+
29
+ Nobody writes `jit_for` except to make a per-cell computation fast, so running it a hundred times slower would answer a question that was not asked, and would hide the difference behind a call that looks the same either way. An expression that can be written without a loop is better written as `CArray.fuse`, which is one pass with or without this gem.
30
+
31
+ ## Four ways to compute an expression, and what separates them
32
+
33
+ CArray can evaluate `out = (a + b) * (c - a) + b * c - a` four ways now, and the line between them is not a factor -- it is what `n` gets multiplied by. `CArray.fuse` builds the expression rather than evaluating it a step at a time, and CArray walks what it built; requiring this gem registers a second evaluator (`CArray::JIT::Expression`), so an expression over an array worth compiling for is compiled instead. Where that line falls is CArray's to decide, not this gem's -- ten thousand cells today. So `fuse` is two rows here, and which one you get depends on whether the gem is installed.
34
+
35
+ ```
36
+ n = 4,000,000
37
+ a + b * c the longer expression
38
+ a + b * c ... 1.30 ns/el 4.42 ns/el
39
+ CArray.fuse, walked 0.99 ns/el 2.13 ns/el
40
+ CArray.fuse, compiled here 0.74 ns/el 0.66 ns/el
41
+ jit_each { out = ... } 0.32 ns/el 0.30 ns/el
42
+ ```
43
+
44
+ An expression of `k` operations is `k` passes over the data, and walking it rather than materialising it does not change that -- it changes what each pass costs, which is why the first two rows still grow with the expression, one more steeply than the other. Compiled, the expression is one pass however long it is, which is why the last two rows do not move at all.
45
+
46
+ Memory separates them again, on a different line. Summing `(a + b*1) + (a + b*2) + ...`, peak resident over two arrays of eight million doubles, one array being 61 MB (`benchmark/footprint.rb`):
47
+
48
+ ```
49
+ terms 1 2 3 4 6
50
+ a + b*k, summed +123 +245 +367 +488 +611
51
+ fuse walked, leaning left +122 +184 +185 +183 +184
52
+ fuse walked, leaning right +123 +184 +246 +306 +428
53
+ fuse compiled, leaning left +62 +61 +61 +62 +61
54
+ fuse compiled, leaning right +61 +62 +63 +62 +62
55
+ jit_each +0 +1 +1 +1 +1
56
+ ```
57
+
58
+ **CArray** holds one array per term, exactly: each waits as an array until it is combined.
59
+
60
+ **`fuse` walked** answers to the shape rather than the size, and the shape is which way the tree leans: a binary operation pulls its left operand into the buffer it was handed and takes a scratch only for its right, so leaning left descends for free and leaning right holds one buffer per level. The two rows are the same arithmetic written two ways, and they are not the same measurement. The scratch comes from an arena that keeps buffers warm rather than freeing them, so the peak is also what is still held afterwards.
61
+
62
+ **`fuse` compiled** holds the result and nothing else: one array, whatever the expression says and whichever way it leans -- there is no operand to stage when every operation is in one loop.
63
+
64
+ **A compiled kernel** holds nothing at all, because it was given the array to write into: a value never leaves a register between one operation and the next.
65
+
66
+ Only the columns are comparable, not the rows against some other program's numbers: a peak belongs to a process, and the floor moves with whatever it loaded and allocated first.
67
+
68
+ | | needs | passes | intermediates | reaches a neighbour |
69
+ | --- | --- | --- | --- | --- |
70
+ | `a + b * c` | -- | one per operation | one per operation | -- |
71
+ | `CArray.fuse`, walked | -- | one per operation | one per right-spine level | -- |
72
+ | `CArray.fuse`, compiled | a C compiler, or it walks | **one** | **none** | -- |
73
+ | `CArray.jit_each` | a C compiler | **one** | **none** | no |
74
+ | `CArray.jit_for` | a C compiler | **one** | **none** | **yes** |
75
+
76
+ The last two need a compiler outright: without one they raise rather than run slowly, because a block written for them is written to be compiled and a Ruby loop over a million cells is not an answer. `CArray.fuse` never raises for the want of one -- it asks whatever evaluator is registered and walks the expression itself when the answer is nobody, which is what makes it the thing to write where an expression over whole arrays is the whole of it.
77
+
78
+ The last two are the same machinery and differ in what can be said, not in speed: a recurrence, a stencil and a reduction are `jit_for`'s and are not one pass in any of the others -- they are not expressible in them at all.
79
+
80
+ What the table does not show is the compiling, which is about 250 ms the first time and 4 ms in a later process that finds the object on disk. For one pass over a small array the plain expression wins on wall clock and always will; see [What compiling costs](04_Compiling.md#what-compiling-costs-and-where-kernels-are-kept) for where the line falls.