ruby Ractor

Table of Contents

gauche test

何でも揃えてあるgaucheにテストのフレームワークは用意されてるか? Gauche — Practical Scheme Scripting

おや、看板が一新されてるぞ。rubyと同じ様に何かの記念? ひょっとしたら 1/4 世紀 経過しました、ぐらい?

9.34 gauche.test - 単体テスト

やっぱり提供されてた。凄いなあ。じっくり拝見しましょ。

object dump

簡単にオブジェクトを点検できるらしいので、emacsから確認できる様にしてみた。

require "json"
require "objspace"

def dump(obj)
  JSON.pretty_generate(JSON.parse(ObjectSpace.dump(obj)))
end

str = '日本'
puts dump(str)

inf-rubyを入れると、 inf-ruby (C-c C-s)でirbを起動、C-c C-q で終了する様になる。ソース窓の所で ruby-load-current-file (C-c C-k) すれば結果がirbの窓に出てくる。

irb(main):001> {
  "address": "0x7fc9e6ce3af8",
  "type": "STRING",
  "shape_id": 0,
  "slot_size": 40,
  "class": "0x7fca029eec50",
  "embedded": true,
  "chilled": true,
  "bytesize": 6,
  "encoding": "UTF-8",
  "coderange": "valid",
  "memsize": 40,
  "flags": {
    "wb_protected": true,
    "age": 0
  }
}
=> true

こんな評価結果が表示された。

oracle problem

前回の最後にチャッピー君が話題を拡げる為、期待値を持たない検証手法(oracle problem) 面白いですよと振ってきた。初見なので馬鹿にされない様に下調べしとく。だって、 銀座のママ宜しく、教養を試している節が有るからね。

「正解が分からない」アルゴリズムをどうテストするか

これを拝読すると、テスト・コードを必ず記述しましょとは全く違う次元の 話だって事が良くわかる。普通のテストは、リファクタリングしても機能が損傷しなかったかの 確認ってか、免罪符でテストしてるのかな(猛抗議を受けそう)。

Open3

前回のコード・リーディングで解った積りになってた部分を取り出してきて、 味わってみる。

def run_program(cmd, input)
  stdout, stderr, status = nil

  Timeout.timeout(TIMEOUT_SEC) do
    stdout, stderr, status = Open3.capture3(cmd, stdin_data: input)
  end

  unless status.success?
    raise "runtime error (exit=#{status.exitstatus}): #{stderr.strip}"
  end

  parse_output(stdout)
rescue Timeout::Error
  raise "timeout"
end

ここから読み取れるのは、終了ステータスが成功じゃなかった場合、runtime errorと 判定して、詳細な事は、stderrを参照できるって事だ。エラー処理も綺麗に記述 できる物だね。何かの時の参考にしよう。

caputure3ってOS的には、forkしてexecして親子の絆を結んで通信。更にはwaitで、 子の終了を待つっていう複雑な事をしてるはず。lib/open3.rbを見たら、さりげない 風に定義されてたぞ。IOは、 out_reader = Thread.new { o.read } こんな風に、 待ちになったら、他に譲るっていう無駄の無い設計になってた。

Ractor

gaucheの説明書を見てたら、 9.35 gauche.threads - スレッド もサポートしてた。使い所と注意点も説明されてて嬉しい。 そんじゃrubyはと探してみたら、Threadの更に上を行く機能を発見。 その名はRactor。

Threadって所詮1つのCPUでの実行でしかなかった。Ractorは、複数のCPU でタスクを実行するんで、パソコンをこき使う感がして得した気分になりそう。 実例が出てたので確かめてみる。

sakae@lu:tmp$ ruby rac.rb
処理開始
rac.rb:18: warning: Ractor API is experimental and may change in future versions of Ruby.
タスクA: 開始
タスクB: 開始
タスクB: 終了 (5.385616974s)
タスクA: 終了 (5.396340336s)
全処理終了 (5.396918182s)

まだ実験の最中だからって断りが出てきた。3.X時代はtakeで待ち合わせなんだけど、 ruby 4.0.0では、joinになってる。

# 重い計算処理(CPUバウンドなタスク)
def cpu_heavy_task(task_name)
  puts "#{task_name}: 開始"
  start_time = Time.now
  100_000_000.times do |i|
    dmy = i * i
  end
  end_time = Time.now
  puts "#{task_name}: 終了 (#{end_time - start_time}s)"
  end_time - start_time
end

puts "処理開始"
start_total_time = Time.now

r1 = Ractor.new { cpu_heavy_task("タスクA") }
r2 = Ractor.new { cpu_heavy_task("タスクB") }
result1 = r1.join
result2 = r2.join

end_total_time = Time.now
puts "全処理終了 (#{end_total_time - start_total_time}s)"

Ractorとなっている所をThreadに変更すると、1CPUの能力を分けあって動く コードになる。

処理開始
タスクA: 開始
タスクB: 開始
タスクA: 終了 (10.412197686s)
タスクB: 終了 (10.225076968s)
全処理終了 (10.426710411s)

その結果、処理時間は倍になる。勿論、タスクを1つだけにすれば、処理時間は 半分になるのは、言うまでも無い(って、実行してるじゃん)。

処理開始
タスクA: 開始
タスクA: 終了 (5.13967108s)
全処理終了 (5.140015627s)

先輩はgolangだ。goと言う魔法の語句を付けるだけで、複数CPUを利用してくれる。 開発当初からサポートされてるんで、安定してるぞ(オイラーは使用実績無しですけど)。

package main

import (
	"fmt"
	"sync"
	"time"
)

func cpuHeavyTask(taskName string, wg *sync.WaitGroup) {
	defer wg.Done() // タスク完了時にWaitGroupに通知
	fmt.Printf("%s: 開始\n", taskName)
	startTime := time.Now()
	for i := 0; i < 10_000_000_000; i++ {
		_ = i * i
	}
	endTime := time.Now()
	fmt.Printf("%s: 終了 (%v)\n", taskName, endTime.Sub(startTime))
}

func main() {
	fmt.Println("処理開始")
	startTime := time.Now()

	var wg sync.WaitGroup // 終了待ちのためのWaitGroup
	wg.Add(2)
	go cpuHeavyTask("タスクA", &wg)
	go cpuHeavyTask("タスクB", &wg)
	wg.Wait()

	endTime := time.Now()
	fmt.Printf("全処理終了 (%v)\n", endTime.Sub(startTime))
}

goroutineの実装 これを見ると、src/runtimeに実装されてるんだね。

io.Readerをすこれ 上記を探してる時、見つけたもの。interfceはgoの 隠れた銘品だそうです。

group_by

前回のblack-test結果の一部。

--- Test: single ---
genA: ERROR: invalid output line: "a   1   \n"
genB: OK (1 symbols)
genC: OK (1 symbols)
genD: OK (1 symbols)
✅ Majority result (2/3 agree)
Agreeing: genB, genC

良く見ると、3人の候補のうち、2人(genB, genC)の意見が一致し、genDと意見が 割れている。

参考に huffman.go にも参戦してもらう。作成者曰く、メタモルフィック関係の考慮 と言う事で、ハフマン符号は左右の枝に0と1のどちらを割り当てるかで結果が変わるため、この実装では左を . (0)、右を - (1) に固定しています。 の説明が有った。

rdbgで92行目にBPを置いて確認。

[87, 96] in test.rb
    87|   if outputs.size < 2
    88|     warn "Not enough successful programs for voting"
    89|     next
    90|   end
    91|
=>  92|   winner, votes = majority_vote(outputs)
    93|
    94|   if votes == 1
    95|     warn "❌ No majority agreement"
    96|     outputs.each do |prog, out|
=>#0    block {|name=:single, input="a"|} in <main> at test.rb:92
  #1    [C] Hash#each at #<none>:0
  # and 1 frames (use `bt' command for all frames)

Stop by #0  BP - Line  /tmp/huffman/test.rb:92 (line)
(rdbg) pp outputs    # command
{genB: {"a" => [1, "."]},
 genC: {"a" => [1, "."]},
 genD: {"a" => [1, "-"]},
 golang: {"a" => [1, "."]}}

genDの結果が他と異なっていた。

mruby

OpenBSDではportsになってるんだけど、何となくREADMEを眺めていたら、 rakeでも大丈夫そうだったので、試してみた。

https://github.com/mruby/mruby/archive/refs/tags/3.3.0/mruby-3.3.0.tar.gz

ob$ rake
 :
gems/mruby-bin-strip/tools/mruby-strip/mruby-strip.o
LD    build/host/bin/mruby-strip
ld: warning: parse.y:4910 (mrbgems/mruby-compiler/core/parse.y:4910)(y.tab.o:(p
arse_string) in archive /tmp/mruby-3.3.0/build/host/lib/libmruby.a): warning: s
trcat() is almost always misused, please use strlcat()
GEN   build/host/bin/mruby-strip -> bin/mruby-strip

Build summary:

================================================
      Config Name: host
 Output Directory: build/host
         Binaries: mrbc, mruby-config
    Included Gems:
             mruby-array-ext - Array class extension
             mruby-bigint - Integer class extension to multiple-precision
             mruby-bin-config - mruby-config command
             mruby-bin-debugger - mruby debugger command
               - Binaries: mrdb
       ;
      Config Name: host/mrbc
 Output Directory: build/host/mrbc
         Binaries: mrbc
    Included Gems:
             mruby-bin-mrbc - mruby compiler executable
             mruby-compiler - mruby compiler library

コンパイル時間を計測

2m02.55s real     1m41.32s user     0m15.66s system   ; real i386
5m25.22s real     1m00.33s user     3m18.80s system   ; VMWare(amd64)

仮想マシンの性格が良く表われているな。OSに依頼する事が有るとオーバーヘッドが 非常に過大になるとな。それにIO待ちな時間も多い。 userでの実行ならば、変な介入が無いので、amd64の方が速い。 あるいは、マシンが高速だからだな。 余興でも、こういうデータを取得しておくと参考になるな。

CPP   src/allocf.c -> build/host/src/allocf.pi
CPP   src/array.c -> build/host/src/array.pi
 :
CPP   mrbgems/mruby-compiler/core/y.tab.c -> build/host/mrbgems/mruby-compiler/core/y.tab.pi
CC    mrbgems/mruby-bin-mrbc/tools/mrbc/mrbc.c -> build/host/mrbc/mrbgems/mruby-bin-mrbc/tools/mrbc/mrbc.o
CC    mrbgems/mruby-bin-mrbc/tools/mrbc/stub.c -> build/host/mrbc/mrbgems/mruby-bin-mrbc/tools/mrbc/stub.o
CC    src/allocf.c -> build/host/mrbc/src/allocf.o
CC    src/array.c -> build/host/mrbc/src/array.o
 :
AR    build/host/mrbc/lib/libmruby_core.a
ar: warning: creating /tmp/mruby-3.3.0/build/host/mrbc/lib/libmruby_core.a
LD    build/host/mrbc/bin/mrbc
ld: warning: parse.y:4910 (mrbgems/mruby-compiler/core/parse.y:4910)(y.tab.o:(parse_string) in archive /tmp/mruby-3.3.0/build/host/mrbc/lib/libmruby_core.a): warning: strcat() is almost always misused, please use strlcat()
GEN   mrblib/*.rb -> build/host/mrblib/mrblib.c
 :

そしてコンパイルの過程は上記の様。しっかりbisonの世話になってる。rubyに特化 したパーサーの出番って有るようで無いような、有るみたい(我ながら日本語がおかしいな)。 次は期待のgdb登場。

ob$ gdb -q mruby
Reading symbols from mruby...
(gdb) b write
Breakpoint 1 at 0xdd550
(gdb) r -e 'puts("HELLO")'
Starting program: /tmp/mruby -e 'puts("HELLO")'

(gdb) bt
#0  __i386_read_tcb (offset=0) at /usr/include/machine/tcb.h:41
#1  _libc_write_cancel (fd=1, buf=0x44218000, nbytes=5)
    at /usr/src/lib/libc/sys/w_write.c:26
#2  0x04470a3f in __swrite (cookie=0x243f8480 <__stdout>,
    buf=0x44218000 "HELLO", n=5) at /usr/src/lib/libc/stdio/stdio.c:65
#3  0x044352c8 in __sflush (fp=0x243f8480 <__stdout>)
    at /usr/src/lib/libc/stdio/fflush.c:77
#4  0x0443519a in _libc_fflush (fp=0x243f8480 <__stdout>)
    at /usr/src/lib/libc/stdio/fflush.c:48
#5  0x182a06f0 in mrb_print (mrb=0x71037000, self=...)
    at /tmp/mruby-3.3.0/mrbgems/mruby-print/src/print.c:57
#6  0x182556a3 in mrb_vm_exec (mrb=0x71037000, proc=<optimized out>,
    pc=<optimized out>) at /tmp/mruby-3.3.0/src/vm.c:1902
#7  0x1824ee39 in mrb_vm_run (mrb=0x71037000, proc=0x6a8f3d30, self=...,
    stack_keep=0) at /tmp/mruby-3.3.0/src/vm.c:1331
#8  0x1824e075 in mrb_top_run (mrb=0x71037000, proc=0x6a8f3d30, self=...,
    stack_keep=0) at /tmp/mruby-3.3.0/src/vm.c:3121
#9  0x182672bd in mrb_load_exec (mrb=0x71037000, p=0x513a9010, c=0x6a5ae810)
    at mrbgems/mruby-compiler/core/parse.y:6919
#10 0x182678b2 in mrb_load_nstring_cxt (len=13, mrb=<optimized out>,
    s=<optimized out>, c=<optimized out>)
    at mrbgems/mruby-compiler/core/parse.y:6991
#11 mrb_load_string_cxt (mrb=0x71037000, s=0x6a595800 "puts(\"HELLO\")",
    c=0x6a5ae810) at mrbgems/mruby-compiler/core/parse.y:7003
#12 0x1821d5dc in main (argc=3, argv=0xcf7fb0d4)
    at /tmp/mruby-3.3.0/mrbgems/mruby-bin-mruby/tools/mruby/mruby.c:360

mrbgems/mbuby-print/* って事は、全部gemで組み換え自由って事なのかな。 遺伝子編集ならモンサント社でキャスパー9か。いや、中山先生のiPS細胞による デザーナーベィビーだよ。Matz先生認定のcoreを核にして、ミニマリストのmruby を制作するのも楽しそう。

README

ロゴとネーミングの法律―事業を守る商標のしくみ なんて本を読んだ。

身近な例で解説してくれてるんで、法律の趣旨が良く解った(つもり)よ。 まあ、本当に解ってしまえば、弁理士なんていらなくなるんだけどね。 そんな士業はAIに依頼しちゃえばいいのでは? 著者の弁理士さんも、危機感を 持って、言及(コラムで)されてたぞ。

商標は基本、先願主義。似たようなのはダメよだ。但し分野が違えばOK。 消費者が混同する恐れが無いから。で、こういうのを検索できるようになってるそう。 https://www.j-platpat.inpit.go.jp/

何だググると一緒じゃんト高をくくっていると、数々の罠が有るそうな。やっぱり 弁理士に依頼しましょってのが正解みたい。


This year's Index

Home