carray-jit 0.1.2 → 0.1.3
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 +4 -4
- data/CHANGELOG.md +771 -3
- data/README.md +7 -6
- data/carray-jit.gemspec +1 -3
- data/docs/00_Introduction.md +4 -3
- data/docs/01_GettingStarted.md +1 -1
- data/docs/02_KernelShapes.md +93 -14
- data/docs/03_SupportedFeatures.md +582 -26
- data/docs/04_Compiling.md +33 -6
- data/docs/05_DesignNotes.md +3 -3
- data/docs/06_Cheatsheet.md +198 -5
- data/docs/07_StepByStep.ja.md +534 -0
- data/docs/07_StepByStep.md +535 -0
- data/examples/README.md +12 -0
- data/examples/applications/alarm.rb +121 -0
- data/examples/applications/collatz.rb +105 -0
- data/examples/applications/cubic_spline.rb +331 -0
- data/examples/applications/dithering.rb +144 -0
- data/examples/applications/group_stats.rb +115 -0
- data/examples/applications/lookup.rb +126 -0
- data/examples/applications/median_filter.rb +153 -0
- data/examples/applications/parcel_ascent.rb +220 -0
- data/examples/applications/point_in_polygon.rb +111 -0
- data/examples/applications/random_walk.rb +98 -0
- data/examples/applications/van_der_pol.rb +186 -0
- data/examples/applications/wet_bulb.rb +140 -0
- data/examples/features/10_complex.rb +14 -4
- data/examples/features/15_loops.rb +7 -1
- data/lib/carray/jit/access.rb +14 -0
- data/lib/carray/jit/analyzer.rb +2077 -136
- data/lib/carray/jit/block_reader.rb +37 -6
- data/lib/carray/jit/c_function.rb +613 -76
- data/lib/carray/jit/c_generator.rb +1595 -156
- data/lib/carray/jit/call.rb +68 -0
- data/lib/carray/jit/compiler.rb +75 -11
- data/lib/carray/jit/kernel.rb +369 -32
- data/lib/carray/jit/node.rb +359 -9
- data/lib/carray/jit/sorting_networks.rb +182 -0
- data/lib/carray/jit/type_assignment.rb +371 -34
- data/lib/carray/jit/version.rb +1 -1
- data/lib/carray/jit.rb +560 -64
- metadata +22 -8
- data/ext/carray_jit_access/carray_jit_access.c +0 -460
- data/ext/carray_jit_access/extconf.rb +0 -8
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# `CArray.jit_call`, and only what defining it needs.
|
|
2
|
+
#
|
|
3
|
+
# A file that calls `jit_call` has to have the method, and having it used to
|
|
4
|
+
# mean loading the whole compiler -- the analyzer, the generator, the cache.
|
|
5
|
+
# For a program that compiles, that is what it is. For one whose call sites
|
|
6
|
+
# were built ahead of it by carray-jit-aot, and where a provider answers
|
|
7
|
+
# every one of them, it is a compiler loaded and never reached.
|
|
8
|
+
#
|
|
9
|
+
# So this is the light half: the declaration's parser, which says what a call
|
|
10
|
+
# takes, and the provider, which may answer without compiling. The compiler
|
|
11
|
+
# is required at the first site no provider answers, and not before.
|
|
12
|
+
#
|
|
13
|
+
# `require "carray/jit"` still loads everything, and a program that does not
|
|
14
|
+
# know about any of this sees no difference.
|
|
15
|
+
|
|
16
|
+
require "carray"
|
|
17
|
+
require "carray/jit/version"
|
|
18
|
+
require "carray/jit/errors"
|
|
19
|
+
require "carray/jit/c_function"
|
|
20
|
+
|
|
21
|
+
class CArray
|
|
22
|
+
|
|
23
|
+
# Compiles the block as a C function and calls it, here, with the locals
|
|
24
|
+
# around it:
|
|
25
|
+
#
|
|
26
|
+
# def moving_average (values, window)
|
|
27
|
+
# n = values.elements
|
|
28
|
+
# out = CArray.double(n)
|
|
29
|
+
# CArray.jit_call("void (*)(double *out, const double *values, " \
|
|
30
|
+
# "size_t n, size_t window)") {
|
|
31
|
+
# n.times { |i| ... }
|
|
32
|
+
# }
|
|
33
|
+
# out
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# The declaration's parameter names are the join, and they do the work
|
|
37
|
+
# twice over: they are the body's parameters, so the block declares none,
|
|
38
|
+
# and they name the locals of the method around it that the call reads.
|
|
39
|
+
# In C a parameter's name in a prototype is decoration; here it is the
|
|
40
|
+
# whole binding, and a name with no local behind it is an error at the
|
|
41
|
+
# call rather than a `nil` inside it.
|
|
42
|
+
#
|
|
43
|
+
# What this saves over `jit_function` plus a `call` is the argument list --
|
|
44
|
+
# written out, it is a second statement of what the declaration already
|
|
45
|
+
# said, in an order nothing checks. What it costs is reading those locals
|
|
46
|
+
# through the block's binding, measured at about 0.3 microseconds against a
|
|
47
|
+
# call that costs several.
|
|
48
|
+
#
|
|
49
|
+
# Compiled once per call site and kept, so the second call is a lookup.
|
|
50
|
+
# The block is read, never yielded to; a name it closes over that is not a
|
|
51
|
+
# parameter is refused, as it is for `jit_function`.
|
|
52
|
+
#
|
|
53
|
+
# @param prototype [String] the function's C declaration. `double (*)(...)`
|
|
54
|
+
# is the spelling for one with no name, which is what a call needs: the
|
|
55
|
+
# name would be written once to be looked at never.
|
|
56
|
+
# @return [Object, nil] what the function answered, or nil where it
|
|
57
|
+
# returns `void`.
|
|
58
|
+
# @raise [CArray::JIT::Unsupported] when the block falls outside the
|
|
59
|
+
# recognized subset, or names parameters the declaration already named.
|
|
60
|
+
def self.jit_call (prototype, &block)
|
|
61
|
+
unless block
|
|
62
|
+
raise JIT::Unsupported,
|
|
63
|
+
"`jit_call` compiles a block and calls it, and none was given"
|
|
64
|
+
end
|
|
65
|
+
JIT.call_here(prototype, block)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
end
|
data/lib/carray/jit/compiler.rb
CHANGED
|
@@ -3,6 +3,7 @@ require "fileutils"
|
|
|
3
3
|
require "rbconfig"
|
|
4
4
|
require "tmpdir"
|
|
5
5
|
require "fiddle"
|
|
6
|
+
require "monitor"
|
|
6
7
|
|
|
7
8
|
class CArray
|
|
8
9
|
module JIT
|
|
@@ -207,7 +208,21 @@ class CArray
|
|
|
207
208
|
# kernel does not throw its object away. The file then names the
|
|
208
209
|
# first site that compiled it, which is a place the code was written
|
|
209
210
|
# rather than the only one.
|
|
211
|
+
# One compile at a time in a process. The staging path a build
|
|
212
|
+
# compiles to is named for the process, so two threads building at
|
|
213
|
+
# once wrote the same file and whichever renamed first left the other
|
|
214
|
+
# renaming a path that was gone -- a bare Errno::ENOENT out of three
|
|
215
|
+
# threads in four. Serialising them also spares the second thread the
|
|
216
|
+
# build: it takes the lock after the first has renamed, and finds the
|
|
217
|
+
# object where a later run would. Reentrant, so that a build reached
|
|
218
|
+
# from inside another -- which none is today -- would not stop here.
|
|
219
|
+
LOCK = Monitor.new
|
|
220
|
+
|
|
210
221
|
def build (source, function_name, header: nil, flags: FLAGS)
|
|
222
|
+
LOCK.synchronize { build_locked(source, function_name, header: header, flags: flags) }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def build_locked (source, function_name, header: nil, flags: FLAGS)
|
|
211
226
|
dump(header.to_s + source)
|
|
212
227
|
|
|
213
228
|
directory = prepare(cache_directory)
|
|
@@ -232,13 +247,36 @@ class CArray
|
|
|
232
247
|
# Build to a unique path and rename, so a concurrent process never
|
|
233
248
|
# loads a half-written object.
|
|
234
249
|
staging_path = "#{object_path}.#{Process.pid}"
|
|
235
|
-
|
|
250
|
+
handle =
|
|
251
|
+
begin
|
|
252
|
+
compile(source_path, staging_path, flags)
|
|
253
|
+
# Opened before it is published, under the name only this
|
|
254
|
+
# process knows. Published first, the object stands in the
|
|
255
|
+
# cache for the moment between the rename and the open, and
|
|
256
|
+
# another process pruning in that moment can evict it -- prune
|
|
257
|
+
# spares what the process running it is building, which says
|
|
258
|
+
# nothing about what anyone else is. The build then failed to
|
|
259
|
+
# load what it had just compiled. Opened first, the image is
|
|
260
|
+
# mapped and stays mapped whatever the directory does
|
|
261
|
+
# afterwards, which is why evicting an object in use is safe.
|
|
262
|
+
load_new_object(staging_path)
|
|
263
|
+
rescue Exception
|
|
264
|
+
# A build that did not finish leaves nothing behind. The
|
|
265
|
+
# source is half an entry: nothing looks it up -- a hit is an
|
|
266
|
+
# object -- and prune globs objects too, so it would sit in the
|
|
267
|
+
# cache for good, one per kernel per run against a toolchain
|
|
268
|
+
# that cannot compile. The staging file would be swept in five
|
|
269
|
+
# minutes; it goes now, beside the source it came from.
|
|
270
|
+
remove_entry(object_path)
|
|
271
|
+
unlink(staging_path)
|
|
272
|
+
raise
|
|
273
|
+
end
|
|
236
274
|
File.rename(staging_path, object_path)
|
|
237
275
|
sweep_staging(directory)
|
|
238
276
|
sweep_environments
|
|
239
277
|
prune(directory, object_path)
|
|
240
278
|
|
|
241
|
-
[
|
|
279
|
+
[handle, true]
|
|
242
280
|
end
|
|
243
281
|
|
|
244
282
|
# The flags are part of the key: the same C built two ways is two
|
|
@@ -369,13 +407,28 @@ class CArray
|
|
|
369
407
|
# this user.
|
|
370
408
|
def prepare (directory)
|
|
371
409
|
FileUtils.mkdir_p(directory, :mode => 0700) unless File.directory?(directory)
|
|
372
|
-
|
|
373
|
-
if (mode & 0022) != 0
|
|
410
|
+
stat = File.stat(directory)
|
|
411
|
+
if (stat.mode & 0022) != 0
|
|
374
412
|
raise CompilationError,
|
|
375
413
|
"#{directory} is writable by other users; " \
|
|
376
414
|
"carray-jit loads shared objects from it. " \
|
|
377
415
|
"Fix its permissions or set CARRAY_JIT_CACHE elsewhere."
|
|
378
416
|
end
|
|
417
|
+
# And whose it is, not only who may write to it. A directory of
|
|
418
|
+
# someone else's is one they write to whatever its mode says, and
|
|
419
|
+
# 0700 is what an attacker's own directory would be: a cache root
|
|
420
|
+
# under a shared /tmp, where this user has yet to create the
|
|
421
|
+
# directory for their environment, is a name someone else can take
|
|
422
|
+
# first -- as a directory of their own, or as a symlink to one,
|
|
423
|
+
# which the mode is then read through. Followed on purpose: a
|
|
424
|
+
# cache root symlinked to another volume is an ordinary
|
|
425
|
+
# arrangement, and it is the owner that says whether it is ours.
|
|
426
|
+
unless stat.uid == Process.euid
|
|
427
|
+
raise CompilationError,
|
|
428
|
+
"#{directory} belongs to uid #{stat.uid}, not to this user " \
|
|
429
|
+
"(uid #{Process.euid}); carray-jit loads shared objects " \
|
|
430
|
+
"from it. Set CARRAY_JIT_CACHE to a directory of your own."
|
|
431
|
+
end
|
|
379
432
|
directory
|
|
380
433
|
end
|
|
381
434
|
|
|
@@ -445,13 +498,13 @@ class CArray
|
|
|
445
498
|
|
|
446
499
|
def remove_entry (object_path)
|
|
447
500
|
source_path = object_path.sub(/#{Regexp.escape(shared_object_suffix)}\z/, ".c")
|
|
448
|
-
[object_path, source_path].each
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
501
|
+
[object_path, source_path].each { |path| unlink(path) }
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def unlink (path)
|
|
505
|
+
File.unlink(path) if File.exist?(path)
|
|
506
|
+
rescue StandardError
|
|
507
|
+
nil
|
|
455
508
|
end
|
|
456
509
|
|
|
457
510
|
def touch (path)
|
|
@@ -473,6 +526,17 @@ class CArray
|
|
|
473
526
|
end
|
|
474
527
|
|
|
475
528
|
def compile (source_path, object_path, flags = FLAGS)
|
|
529
|
+
# Asked before the spawn, because a spawn that finds nothing raises
|
|
530
|
+
# Errno::ENOENT naming the program and nothing else -- not what it
|
|
531
|
+
# was wanted for, and not where to say so. This is the same lookup
|
|
532
|
+
# the key already makes to identify the compiler.
|
|
533
|
+
unless resolve_compiler(compiler_command)
|
|
534
|
+
raise CompilationError,
|
|
535
|
+
"`#{compiler_command}` is not an executable this process " \
|
|
536
|
+
"can find; carray-jit compiles its kernels with it. " \
|
|
537
|
+
"Set CARRAY_JIT_CC to a C compiler, or leave it unset for " \
|
|
538
|
+
"the one Ruby was built with."
|
|
539
|
+
end
|
|
476
540
|
command = [compiler_command, *flags, source_path, "-o", object_path, "-lm"]
|
|
477
541
|
output = IO.popen(command, err: [:child, :out]) { |io| io.read }
|
|
478
542
|
unless $?.success?
|