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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +771 -3
  3. data/README.md +7 -6
  4. data/carray-jit.gemspec +1 -3
  5. data/docs/00_Introduction.md +4 -3
  6. data/docs/01_GettingStarted.md +1 -1
  7. data/docs/02_KernelShapes.md +93 -14
  8. data/docs/03_SupportedFeatures.md +582 -26
  9. data/docs/04_Compiling.md +33 -6
  10. data/docs/05_DesignNotes.md +3 -3
  11. data/docs/06_Cheatsheet.md +198 -5
  12. data/docs/07_StepByStep.ja.md +534 -0
  13. data/docs/07_StepByStep.md +535 -0
  14. data/examples/README.md +12 -0
  15. data/examples/applications/alarm.rb +121 -0
  16. data/examples/applications/collatz.rb +105 -0
  17. data/examples/applications/cubic_spline.rb +331 -0
  18. data/examples/applications/dithering.rb +144 -0
  19. data/examples/applications/group_stats.rb +115 -0
  20. data/examples/applications/lookup.rb +126 -0
  21. data/examples/applications/median_filter.rb +153 -0
  22. data/examples/applications/parcel_ascent.rb +220 -0
  23. data/examples/applications/point_in_polygon.rb +111 -0
  24. data/examples/applications/random_walk.rb +98 -0
  25. data/examples/applications/van_der_pol.rb +186 -0
  26. data/examples/applications/wet_bulb.rb +140 -0
  27. data/examples/features/10_complex.rb +14 -4
  28. data/examples/features/15_loops.rb +7 -1
  29. data/lib/carray/jit/access.rb +14 -0
  30. data/lib/carray/jit/analyzer.rb +2077 -136
  31. data/lib/carray/jit/block_reader.rb +37 -6
  32. data/lib/carray/jit/c_function.rb +613 -76
  33. data/lib/carray/jit/c_generator.rb +1595 -156
  34. data/lib/carray/jit/call.rb +68 -0
  35. data/lib/carray/jit/compiler.rb +75 -11
  36. data/lib/carray/jit/kernel.rb +369 -32
  37. data/lib/carray/jit/node.rb +359 -9
  38. data/lib/carray/jit/sorting_networks.rb +182 -0
  39. data/lib/carray/jit/type_assignment.rb +371 -34
  40. data/lib/carray/jit/version.rb +1 -1
  41. data/lib/carray/jit.rb +560 -64
  42. metadata +22 -8
  43. data/ext/carray_jit_access/carray_jit_access.c +0 -460
  44. 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
@@ -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
- compile(source_path, staging_path, flags)
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
- [load_new_object(object_path), true]
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
- mode = File.stat(directory).mode
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 do |path|
449
- begin
450
- File.unlink(path) if File.exist?(path)
451
- rescue StandardError
452
- nil
453
- end
454
- end
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?