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,468 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "fiddle"
|
|
6
|
+
|
|
7
|
+
class CArray
|
|
8
|
+
module JIT
|
|
9
|
+
|
|
10
|
+
# Compiles generated C into a shared object and hands back a loaded
|
|
11
|
+
# function pointer, caching by the hash of the source and the flags used
|
|
12
|
+
# to build it.
|
|
13
|
+
#
|
|
14
|
+
# The cache is on disk and outlives the process because rebuilding is not
|
|
15
|
+
# cheap: about 60 ms to compile, plus roughly 180 ms on macOS the first
|
|
16
|
+
# time a freshly written binary is loaded, which is Gatekeeper checking it
|
|
17
|
+
# rather than anything Ruby does. A second process loading the same file
|
|
18
|
+
# pays 0.2 ms. It is bounded, though -- see #prune -- so it cannot grow
|
|
19
|
+
# without limit the way RubyInline's ~/.ruby_inline does.
|
|
20
|
+
class Compiler
|
|
21
|
+
|
|
22
|
+
# -ffp-contract=off is not optional. Without it clang and gcc fuse
|
|
23
|
+
# `a*b + c` into a single FMA -- even at -O0 on arm64 -- which changes
|
|
24
|
+
# the last bit of the result and breaks agreement with the Ruby
|
|
25
|
+
# evaluator. Measured: a two-term recurrence diverges in 13 of 24
|
|
26
|
+
# cells with contraction on, and matches bit for bit with it off.
|
|
27
|
+
#
|
|
28
|
+
# -O3 rather than -O2, which is what this was until it was measured.
|
|
29
|
+
# The level is what decides whether the loop is vectorised at all, and
|
|
30
|
+
# that is not a clang-shaped question: clang vectorises at -O2 already,
|
|
31
|
+
# so on arm64 the change is worth 1.33x on an element-wise pass and
|
|
32
|
+
# nothing on the rest (seven benchmarks, min of three, none slower).
|
|
33
|
+
# gcc does not vectorise at -O2 at all -- measured 1.6x to 1.7x on the
|
|
34
|
+
# same kernels -- so at -O2 this gem was leaving that on the table
|
|
35
|
+
# wherever gcc is the compiler. Compiling costs the same either way:
|
|
36
|
+
# 57 ms against 58 ms on a generated kernel, min of seven.
|
|
37
|
+
#
|
|
38
|
+
# It is also what CArray builds itself with, and what this gem's own
|
|
39
|
+
# fuse path already inherits from CArray::BUILD_FLAGS. The kernel path
|
|
40
|
+
# being lower was not a decision -- the flag list is the one the first
|
|
41
|
+
# milestone was scaffolded with, and only the contraction flag beside
|
|
42
|
+
# it was ever argued for.
|
|
43
|
+
FLAGS = ["-O3", "-fPIC", "-shared", "-ffp-contract=off"].freeze
|
|
44
|
+
|
|
45
|
+
# Kernels retained on disk. Each costs about 17 KB, so the default is
|
|
46
|
+
# roughly 9 MB -- far more than any real program compiles, but a bound
|
|
47
|
+
# all the same.
|
|
48
|
+
DEFAULT_ENTRY_LIMIT = 512
|
|
49
|
+
|
|
50
|
+
class << self
|
|
51
|
+
|
|
52
|
+
def compiler_command
|
|
53
|
+
ENV["CARRAY_JIT_CC"] || RbConfig::CONFIG["CC"] || "cc"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# True when the cache lives in a temporary directory that goes away
|
|
57
|
+
# with the process.
|
|
58
|
+
def ephemeral?
|
|
59
|
+
!ENV["CARRAY_JIT_NO_CACHE"].nil? || ENV["CARRAY_JIT_CACHE"] == "none"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The root holds one directory per environment; kernels live in the
|
|
63
|
+
# directory for this one.
|
|
64
|
+
def cache_root
|
|
65
|
+
return ephemeral_directory if ephemeral?
|
|
66
|
+
ENV["CARRAY_JIT_CACHE"] ||
|
|
67
|
+
File.join(ENV["XDG_CACHE_HOME"] || File.join(Dir.home, ".cache"),
|
|
68
|
+
"carray-jit")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def cache_directory
|
|
72
|
+
File.join(cache_root, environment_tag)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Kernels are only good for the version that generated them and the
|
|
76
|
+
# architecture they were built for. Keeping those apart means a new
|
|
77
|
+
# release does not spend its cache budget on entries nothing can
|
|
78
|
+
# reach any more, and a home directory shared between machines --
|
|
79
|
+
# over NFS, or between Rosetta and native -- does not have one
|
|
80
|
+
# architecture evicting the other's kernels.
|
|
81
|
+
#
|
|
82
|
+
# CArray's version is in it because a kernel is not only generated for
|
|
83
|
+
# this library: it is handed CArray's memory, on layouts CArray
|
|
84
|
+
# decides, and reaches CArray's own C by address. A kernel that
|
|
85
|
+
# compiles against one version and runs against the next is a silent
|
|
86
|
+
# wrong answer rather than a load error, so the two versions travel
|
|
87
|
+
# together.
|
|
88
|
+
def environment_tag
|
|
89
|
+
"#{JIT::VERSION}-carray#{carray_version}-#{RbConfig::CONFIG['arch']}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# CArray's own version is defined by its extension, and this may be
|
|
93
|
+
# running without it: the command that looks after the cache loads the
|
|
94
|
+
# compiler and nothing else, on purpose, so that a cache can be
|
|
95
|
+
# inspected or cleared when CArray itself will not load. RubyGems
|
|
96
|
+
# knows which version a `require` would activate without activating
|
|
97
|
+
# it, and that is the same one -- except where CArray is on the load
|
|
98
|
+
# path from a checkout, which nothing outside the process could have
|
|
99
|
+
# known either way.
|
|
100
|
+
def carray_version
|
|
101
|
+
return CArray::VERSION if defined?(CArray::VERSION)
|
|
102
|
+
return @carray_version if defined?(@carray_version)
|
|
103
|
+
@carray_version =
|
|
104
|
+
if defined?(Gem::Specification)
|
|
105
|
+
begin
|
|
106
|
+
Gem::Specification.find_by_name("carray").version.to_s
|
|
107
|
+
rescue StandardError
|
|
108
|
+
"none"
|
|
109
|
+
end
|
|
110
|
+
else
|
|
111
|
+
"none"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Directories for versions and architectures no longer in use, so
|
|
116
|
+
# they can be reported and removed as a unit.
|
|
117
|
+
def stale_environments
|
|
118
|
+
root = cache_root
|
|
119
|
+
return [] unless File.directory?(root)
|
|
120
|
+
current = cache_directory
|
|
121
|
+
Dir[File.join(root, "*")].select { |path|
|
|
122
|
+
File.directory?(path) && path != current
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def entry_limit
|
|
127
|
+
limit = ENV["CARRAY_JIT_CACHE_LIMIT"]
|
|
128
|
+
limit ? limit.to_i : DEFAULT_ENTRY_LIMIT
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def entries
|
|
132
|
+
Dir[File.join(cache_directory, "*#{shared_object_suffix}")]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def entry_count
|
|
136
|
+
entries.size
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def byte_size
|
|
140
|
+
Dir[File.join(cache_directory, "*")].sum do |path|
|
|
141
|
+
File.file?(path) ? File.size(path) : 0
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Safe even while a kernel from the cache is in use: unlinking a
|
|
146
|
+
# loaded shared object leaves the mapping intact.
|
|
147
|
+
#
|
|
148
|
+
# Clears this environment's kernels; pass everything: true to remove
|
|
149
|
+
# the directories other versions and architectures left behind too.
|
|
150
|
+
def clear (everything: false)
|
|
151
|
+
removed = clear_directory(cache_directory)
|
|
152
|
+
if everything
|
|
153
|
+
stale_environments.each do |path|
|
|
154
|
+
removed += Dir[File.join(path, "*#{shared_object_suffix}")].size
|
|
155
|
+
begin
|
|
156
|
+
FileUtils.remove_entry(path)
|
|
157
|
+
rescue StandardError
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
removed
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Returns [Fiddle handle, whether it was compiled rather than reused].
|
|
166
|
+
#
|
|
167
|
+
# `header` says where the kernel was written. It is written into the
|
|
168
|
+
# .c file but kept out of the digest: two call sites that produce the
|
|
169
|
+
# same kernel are one cached object, and editing the lines above a
|
|
170
|
+
# kernel does not throw its object away. The file then names the
|
|
171
|
+
# first site that compiled it, which is a place the code was written
|
|
172
|
+
# rather than the only one.
|
|
173
|
+
def build (source, function_name, header: nil, flags: FLAGS)
|
|
174
|
+
dump(header.to_s + source)
|
|
175
|
+
|
|
176
|
+
directory = prepare(cache_directory)
|
|
177
|
+
key = digest(source, flags)
|
|
178
|
+
object_path = File.join(directory, "#{key}#{shared_object_suffix}")
|
|
179
|
+
|
|
180
|
+
if File.exist?(object_path)
|
|
181
|
+
handle = load_shared_object(object_path)
|
|
182
|
+
if handle
|
|
183
|
+
touch(object_path)
|
|
184
|
+
return [handle, false]
|
|
185
|
+
end
|
|
186
|
+
# The cached object is unusable -- a truncated write, a toolchain
|
|
187
|
+
# or OS change. Since the cache outlives the process, failing
|
|
188
|
+
# here would fail every future run as well, so drop it and build
|
|
189
|
+
# again.
|
|
190
|
+
remove_entry(object_path)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
source_path = File.join(directory, "#{key}.c")
|
|
194
|
+
File.write(source_path, header.to_s + source)
|
|
195
|
+
# Build to a unique path and rename, so a concurrent process never
|
|
196
|
+
# loads a half-written object.
|
|
197
|
+
staging_path = "#{object_path}.#{Process.pid}"
|
|
198
|
+
compile(source_path, staging_path, flags)
|
|
199
|
+
File.rename(staging_path, object_path)
|
|
200
|
+
sweep_staging(directory)
|
|
201
|
+
sweep_environments
|
|
202
|
+
prune(directory, object_path)
|
|
203
|
+
|
|
204
|
+
[load_new_object(object_path), true]
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# The flags are part of the key: the same C built two ways is two
|
|
208
|
+
# objects, and need not compute the same last bit.
|
|
209
|
+
def digest (source, flags = FLAGS)
|
|
210
|
+
Digest::SHA256.hexdigest([source,
|
|
211
|
+
compiler_identity,
|
|
212
|
+
flags.join(" "),
|
|
213
|
+
RbConfig::CONFIG["arch"],
|
|
214
|
+
target_identity(flags)].join("\0"))
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Flags that mean "this machine" rather than a named target. What
|
|
218
|
+
# they select is not in the flag's text, so it is not in the key
|
|
219
|
+
# either unless it is asked for.
|
|
220
|
+
NATIVE_FLAGS = /\A-m(?:arch|cpu|tune)=native\z/
|
|
221
|
+
|
|
222
|
+
# What `-march=native` actually chose, for the key.
|
|
223
|
+
#
|
|
224
|
+
# Without this, two machines can agree on every other component --
|
|
225
|
+
# the same source, the same `arch` (`x86_64-linux` spans Nehalem to
|
|
226
|
+
# Sapphire Rapids), the same flag *text*, and the same compiler,
|
|
227
|
+
# since #compiler_identity is a stat of a binary that a shared /usr
|
|
228
|
+
# or a common image makes identical -- and disagree only on which
|
|
229
|
+
# instructions exist. The one with the wider CPU writes an object
|
|
230
|
+
# the other then loads and executes. The cache directory is split
|
|
231
|
+
# by `arch` with a shared home in mind (see #environment_tag); this
|
|
232
|
+
# is the same care one level down.
|
|
233
|
+
#
|
|
234
|
+
# Asked of the compiler rather than the CPU, because what matters is
|
|
235
|
+
# what the compiler will emit, and it is the only thing that knows.
|
|
236
|
+
# The predefined macros carry the target triple and the instruction
|
|
237
|
+
# sets together, so one probe answers all of it.
|
|
238
|
+
#
|
|
239
|
+
# Costs a compiler run, so it is only paid when a flag actually says
|
|
240
|
+
# `native` -- the kernel path names no such flag and pays nothing --
|
|
241
|
+
# and then once per process for each set of flags.
|
|
242
|
+
def target_identity (flags)
|
|
243
|
+
native = flags.select { |flag| flag =~ NATIVE_FLAGS }
|
|
244
|
+
return "" if native.empty?
|
|
245
|
+
|
|
246
|
+
command = compiler_command
|
|
247
|
+
key = [command, native]
|
|
248
|
+
cached = @target_identity
|
|
249
|
+
return cached[1] if cached && cached[0] == key
|
|
250
|
+
|
|
251
|
+
macros = probe_macros(command, native)
|
|
252
|
+
identity = macros ? Digest::SHA256.hexdigest(macros)[0, 16] : "unprobed"
|
|
253
|
+
@target_identity = [key, identity]
|
|
254
|
+
identity
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# The macros the compiler predefines for this target, sorted so that
|
|
258
|
+
# the order it prints them in cannot make two identical targets look
|
|
259
|
+
# different. A compiler that will not answer leaves the key saying
|
|
260
|
+
# so, which is worse than a real answer and better than a key that
|
|
261
|
+
# silently claims two machines are one.
|
|
262
|
+
def probe_macros (command, native)
|
|
263
|
+
argv = [*command.split, *native, "-E", "-dM", "-x", "c", "/dev/null"]
|
|
264
|
+
output = IO.popen(argv, err: File::NULL) { |io| io.read }
|
|
265
|
+
return nil unless $?&.success?
|
|
266
|
+
output.lines.sort.join
|
|
267
|
+
rescue SystemCallError, IOError
|
|
268
|
+
nil
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# The compiler's name is not enough to key on: a cache shared with a
|
|
272
|
+
# toolchain upgrade would keep handing back objects the old compiler
|
|
273
|
+
# produced. Its size and mtime stand in for a version, and cost a
|
|
274
|
+
# stat rather than the ~50 ms of asking it with --version.
|
|
275
|
+
def compiler_identity
|
|
276
|
+
command = compiler_command
|
|
277
|
+
cached = @compiler_identity
|
|
278
|
+
return cached[1] if cached && cached[0] == command
|
|
279
|
+
|
|
280
|
+
path = resolve_compiler(command)
|
|
281
|
+
identity =
|
|
282
|
+
if path
|
|
283
|
+
stat = File.stat(path)
|
|
284
|
+
"#{path}:#{stat.size}:#{stat.mtime.to_i}"
|
|
285
|
+
else
|
|
286
|
+
command
|
|
287
|
+
end
|
|
288
|
+
@compiler_identity = [command, identity]
|
|
289
|
+
identity
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def resolve_compiler (command)
|
|
293
|
+
program = command.split.first
|
|
294
|
+
return nil unless program
|
|
295
|
+
if program.include?(File::SEPARATOR)
|
|
296
|
+
return File.executable?(program) ? program : nil
|
|
297
|
+
end
|
|
298
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |directory|
|
|
299
|
+
candidate = File.join(directory, program)
|
|
300
|
+
return candidate if File.file?(candidate) && File.executable?(candidate)
|
|
301
|
+
end
|
|
302
|
+
nil
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
private
|
|
306
|
+
|
|
307
|
+
def clear_directory (directory)
|
|
308
|
+
return 0 unless File.directory?(directory)
|
|
309
|
+
removed = Dir[File.join(directory, "*#{shared_object_suffix}")].size
|
|
310
|
+
Dir[File.join(directory, "*")].each do |path|
|
|
311
|
+
File.unlink(path) if File.file?(path)
|
|
312
|
+
end
|
|
313
|
+
removed
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def ephemeral_directory
|
|
317
|
+
@ephemeral_directory ||= begin
|
|
318
|
+
directory = Dir.mktmpdir("carray-jit-")
|
|
319
|
+
at_exit do
|
|
320
|
+
begin
|
|
321
|
+
FileUtils.remove_entry(directory)
|
|
322
|
+
rescue StandardError
|
|
323
|
+
nil
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
directory
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# 0700, and refuse a directory others can write to: everything in here
|
|
331
|
+
# gets dlopen'd, so a shared writable cache is a way to run code as
|
|
332
|
+
# this user.
|
|
333
|
+
def prepare (directory)
|
|
334
|
+
FileUtils.mkdir_p(directory, :mode => 0700) unless File.directory?(directory)
|
|
335
|
+
mode = File.stat(directory).mode
|
|
336
|
+
if (mode & 0022) != 0
|
|
337
|
+
raise CompilationError,
|
|
338
|
+
"#{directory} is writable by other users; " \
|
|
339
|
+
"carray-jit loads shared objects from it. " \
|
|
340
|
+
"Fix its permissions or set CARRAY_JIT_CACHE elsewhere."
|
|
341
|
+
end
|
|
342
|
+
directory
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Least-recently-used eviction, by modification time, which #touch
|
|
346
|
+
# keeps current on every reuse.
|
|
347
|
+
def prune (directory, keep)
|
|
348
|
+
limit = entry_limit
|
|
349
|
+
return if limit <= 0
|
|
350
|
+
objects = Dir[File.join(directory, "*#{shared_object_suffix}")]
|
|
351
|
+
return if objects.size <= limit
|
|
352
|
+
|
|
353
|
+
ordered = objects.sort_by { |path| File.mtime(path) rescue Time.at(0) }
|
|
354
|
+
(ordered - [keep]).first(objects.size - limit).each do |path|
|
|
355
|
+
remove_entry(path)
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# A process killed mid-compile leaves `<key>.bundle.<pid>` behind.
|
|
360
|
+
# Those match neither the entry glob nor the prune glob, so without
|
|
361
|
+
# this they would sit in the cache for good. Only swept on a miss,
|
|
362
|
+
# to keep it off the reuse path.
|
|
363
|
+
STAGING_MAXIMUM_AGE = 300
|
|
364
|
+
|
|
365
|
+
def sweep_staging (directory)
|
|
366
|
+
now = Time.now
|
|
367
|
+
Dir[File.join(directory, "*#{shared_object_suffix}.*")].each do |path|
|
|
368
|
+
next unless File.file?(path)
|
|
369
|
+
age = now - File.mtime(path) rescue 0
|
|
370
|
+
next if age < STAGING_MAXIMUM_AGE
|
|
371
|
+
begin
|
|
372
|
+
File.unlink(path)
|
|
373
|
+
rescue StandardError
|
|
374
|
+
nil
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# How long an unused environment's directory is kept. A release or an
|
|
380
|
+
# architecture that stops being used would otherwise sit in the cache
|
|
381
|
+
# for good, since nothing in it is ever reached again.
|
|
382
|
+
DEFAULT_ENVIRONMENT_MAXIMUM_AGE = 30 * 24 * 60 * 60
|
|
383
|
+
|
|
384
|
+
def environment_maximum_age
|
|
385
|
+
days = ENV["CARRAY_JIT_CACHE_MAX_AGE_DAYS"]
|
|
386
|
+
days ? days.to_i * 24 * 60 * 60 : DEFAULT_ENVIRONMENT_MAXIMUM_AGE
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def sweep_environments
|
|
390
|
+
age = environment_maximum_age
|
|
391
|
+
return if age <= 0
|
|
392
|
+
now = Time.now
|
|
393
|
+
stale_environments.each do |path|
|
|
394
|
+
# A directory's own mtime only moves when entries are added or
|
|
395
|
+
# removed, so age it by its newest file instead: one that is
|
|
396
|
+
# being reused, and therefore touched, stays.
|
|
397
|
+
newest = Dir[File.join(path, "*")].map { |entry|
|
|
398
|
+
File.mtime(entry) rescue Time.at(0)
|
|
399
|
+
}.max
|
|
400
|
+
next if newest && (now - newest) < age
|
|
401
|
+
begin
|
|
402
|
+
FileUtils.remove_entry(path)
|
|
403
|
+
rescue StandardError
|
|
404
|
+
nil
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def remove_entry (object_path)
|
|
410
|
+
source_path = object_path.sub(/#{Regexp.escape(shared_object_suffix)}\z/, ".c")
|
|
411
|
+
[object_path, source_path].each do |path|
|
|
412
|
+
begin
|
|
413
|
+
File.unlink(path) if File.exist?(path)
|
|
414
|
+
rescue StandardError
|
|
415
|
+
nil
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def touch (path)
|
|
421
|
+
now = Time.now
|
|
422
|
+
File.utime(now, now, path)
|
|
423
|
+
rescue StandardError
|
|
424
|
+
nil
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def dump (source)
|
|
428
|
+
return unless ENV["CARRAY_JIT_DUMP"]
|
|
429
|
+
warn("--- carray-jit generated source ---")
|
|
430
|
+
warn(source)
|
|
431
|
+
warn("-----------------------------------")
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def shared_object_suffix
|
|
435
|
+
RbConfig::CONFIG["DLEXT"] ? ".#{RbConfig::CONFIG['DLEXT']}" : ".so"
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def compile (source_path, object_path, flags = FLAGS)
|
|
439
|
+
command = [compiler_command, *flags, source_path, "-o", object_path, "-lm"]
|
|
440
|
+
output = IO.popen(command, err: [:child, :out]) { |io| io.read }
|
|
441
|
+
unless $?.success?
|
|
442
|
+
raise CompilationError, "#{command.join(' ')}\n#{output}"
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Returns nil rather than raising, so a bad cache entry can be
|
|
447
|
+
# rebuilt. A freshly compiled object that will not load is a real
|
|
448
|
+
# failure and is raised by #load_new_object.
|
|
449
|
+
def load_shared_object (path)
|
|
450
|
+
Fiddle.dlopen(path)
|
|
451
|
+
rescue Fiddle::DLError
|
|
452
|
+
nil
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def load_new_object (path)
|
|
456
|
+
handle = load_shared_object(path)
|
|
457
|
+
unless handle
|
|
458
|
+
raise CompilationError, "could not load freshly compiled #{path}"
|
|
459
|
+
end
|
|
460
|
+
handle
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
end
|
|
468
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
class CArray
|
|
2
|
+
module JIT
|
|
3
|
+
|
|
4
|
+
# The base of every error this gem raises.
|
|
5
|
+
class Error < StandardError
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# Raised when a kernel falls outside the recognized subset. Callers that
|
|
9
|
+
# want the Ruby evaluator instead of a hard failure rescue this.
|
|
10
|
+
class Unsupported < Error
|
|
11
|
+
|
|
12
|
+
# @return [Prism::Location, nil] where in the block the construct is, or
|
|
13
|
+
# `nil` when the message names no place.
|
|
14
|
+
attr_reader :location
|
|
15
|
+
|
|
16
|
+
# @param message [String] what was refused.
|
|
17
|
+
# @param location [Prism::Location, nil] where it is in the block; when
|
|
18
|
+
# given, the line and column are appended to the message.
|
|
19
|
+
def initialize (message, location = nil)
|
|
20
|
+
@location = location
|
|
21
|
+
if location
|
|
22
|
+
super("#{message} (at line #{location.start_line}, column #{location.start_column})")
|
|
23
|
+
else
|
|
24
|
+
super(message)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Raised when the C compiler rejects generated source, or the shared
|
|
31
|
+
# object cannot be loaded. Never a fallback condition -- it means the
|
|
32
|
+
# generator emitted something wrong.
|
|
33
|
+
class CompilationError < Error
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
end
|
|
37
|
+
end
|