- 投稿日:2020-06-29T23:02:12+09:00
数値を使わず 3 で割った余りを求める
お題
0 以上の整数が文字列として与えられ,それを 3 で割った余りを返す関数なりメソッドなりを定義せよ。この記事では Ruby を使うので,メソッドですね。
ただし,数値は一切使わないこととする。つまり,処理の途中で数値オブジェクトを使ったりしない。当然ながら返り値も文字列とする。
組込みのメソッドが内部で数値オブジェクトを生成していないか分からないので,自分で書いたコードが途中で数値オブジェクトを作っていなければよいことにする。
メソッド名を
remainder_modulo_3
と名付けよう。英語が合ってるかよく分からんけど,remainder は剰余のことで,modulo 3 は「3 で割ったときの」という意味1。以下のようになればいい:
p remainder_modulo_3("0") # => 0 p remainder_modulo_3("8") # => 2 p remainder_modulo_3("12") # => 0手でやる方法
プログラムを考える前に,手計算でやる方法を。
例えば 1234567 を 3 で割った余りは,ひと目見て 1 だと分かる。えっ? どういうこと?
まず,もっと簡単な 87 を 3 で割った余りを考える。真面目に割り算をやってもいいのだが,もっとサボる方法がある。
\begin{eqnarray} 87 &=& 8 \times 10 + 7 \\ &=& 8 \times (9 + 1) + 7 \\ &=& 8 \times 9 + 8 + 7 \end{eqnarray}なんだけれども,$8 \times 9$ は $9$ の倍数だから $3$ で割り切れる。よって,$8 \times 9$ は除外して考えてよくて,結局 $8 + 7$ を $3$ で割った余りを求めればよい。
$8 + 7$ は $15$ なので $3$ で割り切れる。余りは $0$ だ。
同じように考えると 1234567 のような大きな数だって,
\begin{eqnarray} 1234567 &=& 1 \times (999999 + 1) + 2 \times (99999 + 1) + 3 \times (9999 + 1) + 4 \times (999 + 1) + 5 \times (99 + 1) + 6 \times (9 + 1) + 7 \\ &=& (9 の倍数) + 1 + 2 + 3 + 4 + 5 + 6 + 7 \end{eqnarray}となるので,$1 + 2 + 3 + 4 + 5 + 6 + 7$ を $3$ で割った余りを求めればいい。
$1 + 2 + 3 + 4 + 5 + 6 + 7$ を真面目に計算する必要はなくて,
(1 + 2) + (3) + (4 + 5) + (6) + 7のように $3$ の倍数が見えてるから,これらを除外して $7$ を $3$ で割った余りを求め,$1$ が得られる。
では $78932705$ のような数はどうか。この数の代わりに
7 + 8 + 9 + 3 + 2 + 7 + 0 + 5を考えればいいのだが,これらの数字のうち,$9$,$3$,$0$ は $3$ の倍数だから除外してよくて,
7 + 8 + 2 + 7 + 5を考えればよい。
また,$7 = 2 \times 3 + 1$ などを考えると,$7$ を $1$ に,$8$ を $2$ に,$5$ を $2$ に置き換えた1 + 2 + 2 + 1 + 2を考えればいいわけだ。(出てくる数字は 1 と 2 しかあり得ない)
人間ならこれをパッと見て $1$ を $2$ を組み合わせて $3$ を作って除外し,余りは $2$ だとすぐに分かる。
しかし,これをプログラムによる文字列処理をやるには?やはりソートして
1 + 1 + 2 + 2 + 2とし,
"11222"
という文字列を作るのがいいだろう。あとは
"111"
とか"222"
とか"12"
などの文字列を削除していけばよさそうだ。コード
こんなんできました:
def remainder_modulo_3(number) s = number.delete("0369").tr("4578", "1212") .chars.sort.join .gsub(/(.)\1\1|1122|12/, "") {"" => "0", "1" => "1", "11" => "2", "2" => "2", "22" => "1"}[s] end文字列で 0 以上の整数を与え,文字列で
"0"
,"1"
,"2"
のいずれかを返す。ちょっと試してみよう:
1000000.times do |n| puts n unless (n % 3).to_s == remainder_modulo_3(n.to_s) end普通に Integer で計算したものと,remainder_modulo_3 メソッドで得たものを比較して,一致してなければそれを表示する。
やってみたところ何も表示されなかったので,コードは合っているようだ。解説
ちょっとメソッドチェーンが長いけれども,やっていることは単純。
まず
delete("0369")だが,0, 3, 6, 9 は 3 の倍数なので,これらの数字は除外する。
次の
tr("4578", "1212")は,例えば 4 は 3 + 1 だから 1 に置き換えてよい,5 は 2 に置き換えてよい,ということ。
その次の
chars.sort.joinは,まず文字列を 1 文字ずつバラし,それをソートして再び繋げる。
これにより,例えば"212"
から"122"
が得られる。この時点で得られる文字列は,「0 文字以上の
"1"
のあとに 0 文字以上の"2"
が続く」というものになる。それ以外のパターンはあり得ない。その次の
gsub(/(.)\1\1|1122|12/, "")は,特定の文字列を左から順に削除しているのだが,正規表現がちょっとややこしい。
ハイライトは
(.)\1\1
の部分。ここは後方参照を使っている。(.)
でキャプチャーした文字がそのあとに二つ続くことを意味し,ここ全体としては「同じ数字が三つ並んだもの」にマッチする。それから,
"1"
の列と"2"
の列の境界部分に存在しうる"1122"
と"12"
にもマッチする。これらを削除するわけだ。
この変換のあと得られる文字列はどんなものがありうるか。
ちょっと退屈だが,"1"
の個数で場合分けしてみよう。まず,
"111"
が真っ先に削除されるので(3 の倍数個の"1"
が削除されるから)"1"
の個数は 0, 1, 2 個の場合だけ考えればよいと分かる。
"1"
が 0 個のとき,"2"
が 0 個以上連なった文字列である。3 の倍数個の"2"
が削除されることを思えば,""
,"2"
,"22"
の三つがありうる。次に
"1"
が 1 個のとき,うしろに続く"2"
が 0 個なら"1"
だし,"2"
が 1 個なら全体("12"
)が削除されて""
になり,"2"
が 2 個なら"2"
となる。"2"
が 3 個以上のときは"222"
が削除されるので,考えなくてよい(可能性は尽きている)。つまり,"1"
,""
,"2"
の三つがありうる。最後に
"1"
が 2 個のとき,うしろに続く"2"
が 0 個なら,"11"
だし,"2"
が 1 個なら"12"
が削除されて"1"
になるし,"2"
が 2 個なら全体("1122"
)が削除されて空文字列になる。"2"
が 3 個なら"1122"
が削除されて"2"
となる。4 個以上は考えなくてよい。つまり,"11"
,"1"
,""
,"2"
の四つがありうる。ふぅ,めんどくさかった。
結局,変換後の文字列は
""
"1"
"11"
"2"
"22"
の五つの可能性しかない。
これらについてのあまりをハッシュで導いている。
以上。応用
9 で割った余りも全く同様にできる。
2 や 5 で割った余りは最後の桁(1 の位)だけを見ればいい(10 が 2 や 5 で割り切れるから)。
4 で割った余りは最後の 2 桁を見る(100 が 4 で割り切れるから)。
8 で割った余りは最後の 3 桁を見る(1000 が 8 で割り切れるから)。
6 で割った余りはだいぶややこしいはず。いずれ挑戦してみたい。
7 で割った余りを数値オブジェクトを生成せずに求めることができるのかは,よく分からない。
最後に
「だからなに?」と言われても答えに窮する。
「整数論」のタグを付けておいたけど,そんな大したものじゃないし,実用性も無いし,すごく面白いというわけでもない。
なにげに思い立ってやってみただけ。
modulo 3 は,正式な数学の用語では「3 を法として」と言う。 ↩
- 投稿日:2020-06-29T22:59:10+09:00
Kinx ライブラリ - JIT ライブラリ
Kinx ライブラリ - JIT ライブラリ
はじめに
「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。JIT コンパイルのためのライブラリを作ってみました。
- 参考
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
- 個別記事へのリンクは全てここに集約してあります。
- リポジトリ ... https://github.com/Kray-G/kinx
- Pull Request 等お待ちしております。
JIT やりたいよね。今回は Kinx - native でも使っている SLJIT を使いやすくしようということで、ライブラリ化しました。SLJIT 自体、ドキュメントが少なくてソースから解読して使っているので、SLJIT そのものの使い方を備忘録的に書こうかとも思ったのだけど、今回は保留。どこかでやるかもしれない。
しかし、SLJIT をそのまま使うよりも勿論使いやすくなっているので、こっちのほうが良いと思う。ホスト言語もスクリプト なので、手軽に楽しめるでしょう。
どんな感じ?
先にどんな感じのプログラムになるか、サンプルを出しておきます。色々細かい話が続いてここまでたどり着いてくれなさそうなので。。。
using Jit; var c = new Jit.Compiler(); var entry1 = c.enter(); var jump0 = c.ge(Jit.S0, Jit.IMM(3)); c.ret(Jit.S0); var l1 = c.label(); c.sub(Jit.R0, Jit.S0, Jit.IMM(2)); c.call(entry1); c.mov(Jit.S1, Jit.R0); c.sub(Jit.R0, Jit.S0, Jit.IMM(1)); c.call(entry1); c.add(Jit.R0, Jit.R0, Jit.S1); c.ret(Jit.R0); jump0.setLabel(l1); var code = c.generate(); for (var i = 1; i <= 42; ++i) { var tmr = new SystemTimer(); var r = code.run(i); System.println("[%8.3f] fib(%2d) = %d" % tmr.elapsed() % i % r); }
Jit.Compiler
オブジェクトを作って、enter
で関数エントリを作り、色々レジスタをいじくりまわしてret
するコードを書きます。で、実行するときはgenerate()
してrun()
、となります。generate()
してdump()
とすると、アセンブル・リストを見ることもできます。色々とばしたい方は サンプル へ Go! → サンプルでは Ruby、Python、PyPy とのベンチマークもしてますよ。
SLJIT
そもそも SLJIT とは何か。
一言でいえば 抽象化アセンブラ で、一つの書き方で複数の環境をサポートできてしまうという、CPU ごとに異なるので作り直さなければならないというアセンブラの問題を解決してくれるライブラリです。現状サポートしているというプラットフォームは以下の通り。
- SLJIT のサポート・プラットフォーム
- Intel-x86 32
- AMD-x86 64
- ARM 32 (ARM-v5, ARM-v7 and Thumb2 instruction sets)
- ARM 64
- PowerPC 32
- PowerPC 64
- MIPS 32 (III, R1)
- MIPS 64 (III, R1)
- SPARC 32
ただし、ここで紹介する Kinx 版 JIT ライブラリは 64bit しかサポートしていないこと、および x64 Windows と x64 Linux しか確認して(できて)いませんので、そこは悪しからずご了承ください。
公式? 説明文書
私が知る限り、参考になる文書は以下程度しか見つかりませんでした。
- https://zherczeg.github.io/sljit/
- http://ftp.jaist.ac.jp/pub/NetBSD/NetBSD-current/src/sys/external/bsd/sljit/dist/doc/tutorial/sljit_tutorial.html
参考にはなります。
GitHub のリポジトリは以下です。
Jit
さて、Kinx ライブラリとしての JIT ライブラリ。C のまま使うより便利にはしてます。もちろん C のライブラリを使えばもっとより細かく制御できると思いますが、それなりのことはできます。
using Jit
Jit ライブラリは標準組み込みではないため、using ディレクティブを使用して明示的に読み込む。
using Jit;Jit オブジェクト
Jit オブジェクトはパラメータ用のメソッドとコンパイラ・クラスが定義されている。
Jit パラメータ用メソッド
Jit のパラメータとして、即値、レジスタ、メモリアクセスの 3 種類がある。以下の形で利用する。
即値、メモリアクセス
即値、メモリアクセスは以下のメソッドで利用する。
Jit.VAR()
はローカル変数領域を使うための特別なメソッド。スタック領域にローカル変数領域が自動的に確保され、その領域を使う。
メソッド 備考 Jit.IMM(v)
64bit 整数、浮動小数点数どちらも同じ書き方。代入先のレジスタと合わせる。 Jit.VAR(n)
ローカル変数領域。1 変数 8 バイト固定。 Jit.MEM0(address)
address として即値を代入、ただし現在実アドレスをスクリプトから指定できないので、スクリプトからは使えない。 Jit.MEM1(r1, offset)
r1 に指定したレジスタをアドレスとみて、offset 位置(バイト単位)のメモリアドレスを示す。 Jit.MEM2(r1, r2, shift)
shift は 0 なら 1 バイト、1 なら 2 バイト、 2 なら 4 バイト、3 なら 8 バイトを示し、 r1 + r2 * (shiftで示すバイト分)
の位置のメモリアドレスを示す。レジスタ
以下のレジスタを使用可能。関数内で使えるレジスタの数は自動的に計算され、関数(
enter()
で区切った範囲)ごとに変わる。
レジスタ 用途 Jit.R0
~Jit.R5
汎用レジスタ。一時的に利用。別関数呼び出し後に破棄される可能性あり。 Jit.S0
~Jit.S5
汎用レジスタ。別関数呼び出し後に破棄されない保証。 Jit.FR0
~Jit.FR5
浮動小数点レジスタ。一時的に利用。別関数呼び出し後に破棄される可能性あり。 Jit.FS0
~Jit.FS5
浮動小数点レジスタ。別関数呼び出し後に破棄されない保証。 尚、Floating Point 用のレジスタは
FR
/FS
合わせて最大で 6 個までなので、FR4
まで使用した場合、FS0
しか使えません。FR5
まで使うとFS*
は全て使えません。以下のような感じですのでご注意を。
FR*
レジスタFS*
レジスタ(使えない) FS0
,FS1
,FS2
,FS3
,FS4
,FS5
FR0
FS0
,FS1
,FS2
,FS3
,FS4
FR0
,FR1
FS0
,FS1
,FS2
,FS3
FR0
,FR1
,FR2
FS0
,FS1
,FS2
FR0
,FR1
,FR2
,FR3
FS0
,FS1
FR0
,FR1
,FR2
,FR3
,FR4
FS0
FR0
,FR1
,FR2
,FR3
,FR4
,FR5
(使えない) Jit コンパイラ
Jit 命令を作っていくためには Jit コンパイラ・オブジェクトを作成する。
var c = new Jit.Compiler();Jit コンパイラには以下のメソッドがある。
Jit コンパイラ・メソッド 復帰値 概要 Jit.Compiler#label()
label 現在の場所にラベルを付加する。 Jit.Compiler#makeConst(reg, init)
ConstTarget コード生成後に即値を設定するための仮定義コードを出力する。 Jit.Compiler#localp(dst, offset)
ローカル変数の実アドレスを取得するコードを出力する。 dst
に示したレジスタに格納される。offset はローカル変数番号。Jit.Compiler#enter(argType)
label 関数の入り口を作成。引数タイプを指定できる(省略可)。 Jit.Compiler#fastEnter(reg)
label 関数の入り口を作成。ただし、余計なエピローグ、プロローグは出力せず、復帰アドレスを reg
に保存する。Jit.Compiler#ret(val)
Return コードを出力する。 val
を返す。val
は浮動小数点数はFR0
レジスタ、それ以外はR0
レジスタで返却される。Jit.Compiler#f2i(dst, op1)
double を int64_t にキャストするコードを出力する。 dst
は汎用レジスタ。op1
は浮動小数点レジスタ。Jit.Compiler#i2f(dst, op1)
int64_t を double にキャストするコードを出力する。 dst
は浮動小数点レジスタ。op1
は汎用レジスタ。Jit.Compiler#mov(dst, op1)
dst
にop1
を代入するコードを出力する。浮動小数点とそれ以外の型は自動的に認識する。Jit.Compiler#neg(dst, op1)
op1
の符号反転した結果をdst
に格納するコードを出力する。Jit.Compiler#clz(dst, op1)
op1
の先頭から 0 であるビットの数を数え、dst
に格納するコードを出力する。Jit.Compiler#add(dst, op1, op2)
op1
とop2
を加算した結果をdst
に格納するコードを出力する。Jit.Compiler#sub(dst, op1, op2)
op1
とop2
を減算した結果をdst
に格納するコードを出力する。Jit.Compiler#mul(dst, op1, op2)
op1
とop2
を乗算した結果をdst
に格納するコードを出力する。Jit.Compiler#div(dst, op1, op2)
浮動小数点数のみ、 op1
とop2
を除算した結果をdst
に格納するコードを出力する。Jit.Compiler#div()
汎用レジスタで符号なしとして除算した値を R0
レジスタに格納するコードを出力する。Jit.Compiler#sdiv()
汎用レジスタで符号付きとして除算した値を R0
レジスタに格納するコードを出力する。Jit.Compiler#divmod()
汎用レジスタで符号なしとして除算した値を R0
レジスタに格納し、余りをR1
レジスタに格納するコードを出力する。Jit.Compiler#sdivmod()
汎用レジスタで符号付きとして除算した値を R0
レジスタに格納し、余りをR1
レジスタに格納するコードを出力する。Jit.Compiler#not(dst, op1)
op1
のビット反転した結果をdst
に格納するコードを出力する。Jit.Compiler#and(dst, op1, op2)
op1
とop2
でビット AND した値をdst
に格納するコードを出力する。Jit.Compiler#or(dst, op1, op2)
op1
とop2
でビット OR した値をdst
に格納するコードを出力する。Jit.Compiler#xor(dst, op1, op2)
op1
とop2
でビット XOR した値をdst
に格納するコードを出力する。Jit.Compiler#shl(dst, op1, op2)
op1
をop2
ビット分、左シフトした値をdst
に格納するコードを出力する。Jit.Compiler#lshr(dst, op1, op2)
op1
をop2
ビット分、論理右シフトした値をdst
に格納するコードを出力する。Jit.Compiler#ashr(dst, op1, op2)
op1
をop2
ビット分、算術右シフトした値をdst
に格納するコードを出力する。Jit.Compiler#call(label)
JumpTarget enter()
定義した関数呼び出しを行うコードを出力する。後で呼び出し先を設定する JumpTarget を返す。label
を指定した場合は後から設定する必要はない。Jit.Compiler#fastCall(label)
JumpTarget fastEnter()
で定義した関数呼び出しを行うコードを出力する。後で呼び出し先を設定する JumpTarget を返す。Jit.Compiler#jmp(label)
JumpTarget jmp
コマンドを出力する。label
を指定した場合は後から設定する必要はない。Jit.Compiler#ijmp(dst)
JumpTarget jmp
コマンドを出力する。dst
はアドレスを示すレジスタ、または即値。Jit.Compiler#eq(op1, op2)
JumpTarget op1 == op2
を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。Jit.Compiler#neq(op1, op2)
JumpTarget op1 != op2
を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。Jit.Compiler#lt(op1, op2)
JumpTarget 符号なしとして op1 < op2
を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。Jit.Compiler#le(op1, op2)
JumpTarget 符号なしとして op1 <= op2
を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。Jit.Compiler#gt(op1, op2)
JumpTarget 符号なしとして op1 > op2
を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。Jit.Compiler#ge(op1, op2)
JumpTarget 符号なしとして op1 >= op2
を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。Jit.Compiler#slt(op1, op2)
JumpTarget 符号付きとして op1 < op2
を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。Jit.Compiler#sle(op1, op2)
JumpTarget 符号付きとして op1 <= op2
を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。Jit.Compiler#sgt(op1, op2)
JumpTarget 符号付きとして op1 > op2
を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。Jit.Compiler#sge(op1, op2)
JumpTarget 符号付きとして op1 >= op2
を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。Jit.Compiler#generate()
JitCode コード生成を行う。 Jit.Compiler#enter(argType)
関数の入り口を
enter
メソッドで定義するが、argType
を指定しなかった場合はJit.ArgType.SW_SW_SW
を指定されたものとみなす。引数は 3 つまで(仕様)で、それぞれの型を指定する。
SW
... Signed Word (64bit)UW
... Unsigned Word (64bit)FP
... Floating Point (64bit)実際問題としては受け取るレジスタのビット列が同じになるため
SW
とUW
は変わらないが、もしかしたら将来何か違いを出すかもしれない。尚、最後の引数からSW
は省略可能。なので、以下は全部同じ意味となる。
Jit.ArgType.SW_SW_SW
Jit.ArgType.SW_SW
Jit.ArgType.SW
引数で渡されるレジスタは決まっており、以下のようになっている。
- 呼び出し側
型 第 1 引数 第 2 引数 第 3 引数 Integer Jit.R0
Jit.R1
Jit.R2
Double Jit.FR0
Jit.FR1
Jit.FR2
- 受け取り側
型 第 1 引数 第 2 引数 第 3 引数 Integer Jit.S0
Jit.S1
Jit.S2
Double Jit.FS0
Jit.FS1
Jit.FS2
呼び出し側でセットするレジスタと、受け取り側で受け取るレジスタが異なることに注意。
ConstTarget
ラベルアドレスを
setLabel()
で設定する。
ラベルのアドレスを即値としてレジスタやメモリに格納したいときに使う。あまり使う機会は無いか。ジャンプテーブルの代わりになるかとも思うが、テーブルの作り方がイマイチうまい仕組みを用意できていないので。ちなみに、即値を
setValue()
で設定することもできるが、普通にJit.IMM(100)
とか、浮動小数点数でもJit.IMM(0.1)
とかできるようにしたので、こちらを使う意味はあまりない。仮にジャンプテーブルに使うとした際の例は後述。
JumpTarget
ジャンプ先、もしくは関数コールのためのアドレスを
setLabel()
で設定する。例えば、比較した結果で分岐させる場合、以下のようになる。
var c = new Jit.Compiler(); // 関数のエントリポイント。 c.enter(); // S0レジスタ値 >= 3 var jump0 = c.ge(Jit.S0, Jit.IMM(3)); ... // 条件が偽のときのコード var jump1 = c.jmp(); var label0 = c.label(); ... // 条件が真のときのコード var label1 = c.label(); ... jump0.setLabel(label0); jump1.setLabel(label1);JitCode
generate()
メソッドでコード生成に成功すると、JitCode オブジェクト返る。JitCode オブジェクトのメソッドは以下の通り。尚、引数は 3 つまでしか指定できないことに注意(仕様)。抽象化アセンブラなので、様々なアーキテクチャに対応するために必要な仕様です。必要ならローカル変数領域を確保して、その先頭アドレスを渡すなどの工夫が必要。サンプルは後述。
メソッド 概要 JitCode#run(a1, a2, a3)
復帰値を Integer として受け取る。 JitCode#frun(a1, a2, a3)
復帰値を Double として受け取る。 JitCode#dump()
ジェネレートされたアセンブル・リストを出力する。 サンプル
フィボナッチ数列(再帰版)
では、恒例のフィボナッチ数列を算出する再帰版のコードを書いてみましょう。サンプルとして最初に提示したものそのままです。
var c = new Jit.Compiler(); var entry1 = c.enter(); var jump0 = c.ge(Jit.S0, Jit.IMM(3)); c.ret(Jit.S0); var l1 = c.label(); c.sub(Jit.R0, Jit.S0, Jit.IMM(2)); c.call(entry1); c.mov(Jit.S1, Jit.R0); c.sub(Jit.R0, Jit.S0, Jit.IMM(1)); c.call(entry1); c.add(Jit.R0, Jit.R0, Jit.S1); c.ret(Jit.R0); jump0.setLabel(l1); var code = c.generate(); for (var i = 1; i <= 42; ++i) { var tmr = new SystemTimer(); var r = code.run(i); System.println("[%8.3f] fib(%2d) = %d" % tmr.elapsed() % i % r); }結果は以下の通り。
[ 0.000] fib( 1) = 1 [ 0.000] fib( 2) = 2 [ 0.000] fib( 3) = 3 [ 0.000] fib( 4) = 5 [ 0.000] fib( 5) = 8 [ 0.000] fib( 6) = 13 [ 0.000] fib( 7) = 21 [ 0.000] fib( 8) = 34 [ 0.000] fib( 9) = 55 [ 0.000] fib(10) = 89 [ 0.000] fib(11) = 144 [ 0.000] fib(12) = 233 [ 0.000] fib(13) = 377 [ 0.000] fib(14) = 610 [ 0.000] fib(15) = 987 [ 0.000] fib(16) = 1597 [ 0.000] fib(17) = 2584 [ 0.000] fib(18) = 4181 [ 0.000] fib(19) = 6765 [ 0.000] fib(20) = 10946 [ 0.000] fib(21) = 17711 [ 0.000] fib(22) = 28657 [ 0.000] fib(23) = 46368 [ 0.000] fib(24) = 75025 [ 0.000] fib(25) = 121393 [ 0.001] fib(26) = 196418 [ 0.001] fib(27) = 317811 [ 0.001] fib(28) = 514229 [ 0.002] fib(29) = 832040 [ 0.002] fib(30) = 1346269 [ 0.004] fib(31) = 2178309 [ 0.006] fib(32) = 3524578 [ 0.009] fib(33) = 5702887 [ 0.016] fib(34) = 9227465 [ 0.035] fib(35) = 14930352 [ 0.042] fib(36) = 24157817 [ 0.066] fib(37) = 39088169 [ 0.119] fib(38) = 63245986 [ 0.181] fib(39) = 102334155 [ 0.289] fib(40) = 165580141 [ 0.476] fib(41) = 267914296 [ 0.773] fib(42) = 433494437ちなみに、
fib(42)
の結果を Ruby, Python, PyPy, PHP, HHVM, Kinx, Kinx(native) で計測して比較してみた。JIT ライブラリ版は上記はrun()
の時間しか計ってないので、スクリプト解釈と JIT コード生成まで含め、全て公平にプロセス全体の user time で算出。速い順に並べると以下の通り。やはり JIT で直接ネイティブ・コードを出力させると際立って速い。何気に Kinx(native) が PyPy より速かったのは嬉しい誤算。HHVM とどっこいくらい。スクリプトでは Ruby 速くなったなー。1.8 時代とか知ってると感慨深いですねー。
言語 版数 User 時間 Kinx(Jit-Lib) 0.10.0 0.828 HHVM 3.21.0 2.227 Kinx(native) 0.10.0 2.250 PyPy 5.10.0 3.313 PHP 7.2.24 11.422 Ruby 2.5.1p57 14.877 Kinx 0.10.0 27.478 Python 2.7.15+ 41.125 尚、先の JIT ライブラリでジェネレートされたアセンブル・リストはこちら。Windows と Linux で違うのだが、今回は Linux。
0: 53 push rbx 1: 41 57 push r15 3: 41 56 push r14 5: 48 8b df mov rbx, rdi 8: 4c 8b fe mov r15, rsi b: 4c 8b f2 mov r14, rdx e: 48 83 ec 10 sub rsp, 0x10 12: 48 83 fb 03 cmp rbx, 0x3 16: 73 0d jae 0x25 18: 48 89 d8 mov rax, rbx 1b: 48 83 c4 10 add rsp, 0x10 1f: 41 5e pop r14 21: 41 5f pop r15 23: 5b pop rbx 24: c3 ret 25: 48 8d 43 fe lea rax, [rbx-0x2] 29: 48 89 fa mov rdx, rdi 2c: 48 89 c7 mov rdi, rax 2f: e8 cc ff ff ff call 0x0 34: 49 89 c7 mov r15, rax 37: 48 8d 43 ff lea rax, [rbx-0x1] 3b: 48 89 fa mov rdx, rdi 3e: 48 89 c7 mov rdi, rax 41: e8 ba ff ff ff call 0x0 46: 49 03 c7 add rax, r15 49: 48 83 c4 10 add rsp, 0x10 4d: 41 5e pop r14 4f: 41 5f pop r15 51: 5b pop rbx 52: c3 retConst の例
Const の例として、あえて書くならこんな感じ。ローカル変数にジャンプテーブルを作っているので、毎回テーブルを作り直していてイマイチ。テーブルだけ作成してアドレスを渡せるようなインターフェースを別途用意すれば解決しそうではある(やるかも)。
var c = new Jit.Compiler(); c.enter(); c.mov(Jit.R1, Jit.IMM(-1)); var jump0 = c.slt(Jit.S0, Jit.IMM(0)); var jump1 = c.sgt(Jit.S0, Jit.IMM(3)); var const0 = c.makeConst(Jit.VAR(0)); var const1 = c.makeConst(Jit.VAR(1)); var const2 = c.makeConst(Jit.VAR(2)); var const3 = c.makeConst(Jit.VAR(3)); // ローカル変数のアドレスを S0 レジスタ(第一引数)のオフセットで取得し R0 レジスタに格納。 c.localp(Jit.R0, Jit.S0); // ローカル変数の値自体を取得。 c.mov(Jit.R0, Jit.MEM1(Jit.R0)); // ローカル変数の中身をアドレスと見立ててジャンプ。 c.ijmp(Jit.R0); var l0 = c.label(); c.mov(Jit.R1, Jit.IMM(102)); c.ret(Jit.R1); var l1 = c.label(); c.mov(Jit.R1, Jit.IMM(103)); c.ret(Jit.R1); var l2 = c.label(); c.mov(Jit.R1, Jit.IMM(104)); c.ret(Jit.R1); var l3 = c.label(); c.mov(Jit.R1, Jit.IMM(105)); var l4 = c.label(); c.ret(Jit.R1); // ジャンプアドレスはコード生成前にセットする。 jump0.setLabel(l4); jump1.setLabel(l4); var code = c.generate(); // const 値はコード生成後にセットする。 const0.setLabel(l0); const1.setLabel(l1); const2.setLabel(l2); const3.setLabel(l3); for (var i = -1; i < 5; ++i) { var r = code.run(i); System.println(r); }結果。
-1 102 103 104 105 -1コード出力はこんな感じ。こちらは試しに Windows 版で出してみた。
0: 53 push rbx 1: 56 push rsi 2: 57 push rdi 3: 48 8b d9 mov rbx, rcx 6: 48 8b f2 mov rsi, rdx 9: 49 8b f8 mov rdi, r8 c: 4c 8b 4c 24 b0 mov r9, [rsp-0x50] 11: 48 83 ec 50 sub rsp, 0x50 15: 48 c7 c2 ff ff ff ff mov rdx, 0xffffffffffffffff 1c: 48 83 fb 00 cmp rbx, 0x0 20: 0f 8c 94 00 00 00 jl 0xba 26: 48 83 fb 03 cmp rbx, 0x3 2a: 0f 8f 8a 00 00 00 jg 0xba 30: 49 b9 95 ff 57 61 89 01 00 00 mov r9, 0x1896157ff95 3a: 4c 89 4c 24 20 mov [rsp+0x20], r9 3f: 49 b9 a7 ff 57 61 89 01 00 00 mov r9, 0x1896157ffa7 49: 4c 89 4c 24 28 mov [rsp+0x28], r9 4e: 49 b9 b9 ff 57 61 89 01 00 00 mov r9, 0x1896157ffb9 58: 4c 89 4c 24 30 mov [rsp+0x30], r9 5d: 49 b9 cb ff 57 61 89 01 00 00 mov r9, 0x1896157ffcb 67: 4c 89 4c 24 38 mov [rsp+0x38], r9 6c: 48 8d 44 24 20 lea rax, [rsp+0x20] 71: 48 6b db 08 imul rbx, rbx, 0x8 75: 48 03 c3 add rax, rbx 78: 48 8b 00 mov rax, [rax] 7b: ff e0 jmp rax 7d: 48 c7 c2 66 00 00 00 mov rdx, 0x66 84: 48 89 d0 mov rax, rdx 87: 48 83 c4 50 add rsp, 0x50 8b: 5f pop rdi 8c: 5e pop rsi 8d: 5b pop rbx 8e: c3 ret 8f: 48 c7 c2 67 00 00 00 mov rdx, 0x67 96: 48 89 d0 mov rax, rdx 99: 48 83 c4 50 add rsp, 0x50 9d: 5f pop rdi 9e: 5e pop rsi 9f: 5b pop rbx a0: c3 ret a1: 48 c7 c2 68 00 00 00 mov rdx, 0x68 a8: 48 89 d0 mov rax, rdx ab: 48 83 c4 50 add rsp, 0x50 af: 5f pop rdi b0: 5e pop rsi b1: 5b pop rbx b2: c3 ret b3: 48 c7 c2 69 00 00 00 mov rdx, 0x69 ba: 48 89 d0 mov rax, rdx bd: 48 83 c4 50 add rsp, 0x50 c1: 5f pop rdi c2: 5e pop rsi c3: 5b pop rbx c4: c3 ret7b 行目の
jmp rax
がポイント。テーブルを静的に定義できるようになればジャンプテーブルとして機能するようになるかと(今は簡単にできる方法が無い...)。4 つ以上の引数の例
ちょっと面倒くさいが、4 つ以上引数を渡したい場合は、ローカル変数領域に値を格納し、そのアドレス(ポインタ)を引数として渡す。以下の例では、最初に引数をローカル変数領域にセットするためのフック関数を経由させている。ちなみに、ローカル変数は全て 8 バイトで確保されるため、直接
Jit.MEM1()
などでアクセスする場合のオフセットは 8 の倍数でないと合わないので注意。var c = new Jit.Compiler(); var entry1 = c.enter(); c.mov(Jit.VAR(0), Jit.S0); c.mov(Jit.VAR(1), Jit.IMM(3)); c.mov(Jit.VAR(2), Jit.IMM(2)); c.mov(Jit.VAR(3), Jit.IMM(1)); c.localp(Jit.R0); var call1 = c.call(); c.ret(Jit.R0); var entry2 = c.enter(); c.mov(Jit.R1, Jit.S0); c.mov(Jit.S0, Jit.MEM1(Jit.R1, 0)); var jump0 = c.ge(Jit.S0, Jit.MEM1(Jit.R1, 8)); c.ret(Jit.S0); var l1 = c.label(); c.sub(Jit.R3, Jit.S0, Jit.MEM1(Jit.R1, 16)); c.mov(Jit.VAR(0), Jit.R3); c.mov(Jit.VAR(1), Jit.IMM(3)); c.mov(Jit.VAR(2), Jit.IMM(2)); c.mov(Jit.VAR(3), Jit.IMM(1)); c.localp(Jit.R0); c.call(entry2); c.mov(Jit.S1, Jit.R0); c.sub(Jit.R3, Jit.S0, Jit.MEM1(Jit.R1, 24)); c.mov(Jit.VAR(0), Jit.R3); c.mov(Jit.VAR(1), Jit.IMM(3)); c.mov(Jit.VAR(2), Jit.IMM(2)); c.mov(Jit.VAR(3), Jit.IMM(1)); c.localp(Jit.R0); c.call(entry2); c.add(Jit.R0, Jit.R0, Jit.S1); c.ret(Jit.R0); jump0.setLabel(l1); call1.setLabel(entry2); var code = c.generate(); for (var i = 1; i <= 42; ++i) { var tmr = new SystemTimer(); var r = code.run(i); System.println("[%8.3f] fib(%2d) = %d" % tmr.elapsed() % i % r); }出力はさっきと同じ。
Double の引数と復帰値
Double 紹介していないのでそれも。こちらもフィボナッチでいきましょう。しかし、俺はフィボナッチ大好きだな。気づかなかったけど。0.1 刻みバージョンです。
var c = new Jit.Compiler(); var entry1 = c.enter(Jit.ArgType.FP); c.mov(Jit.FR0, Jit.IMM(0.3)); var jump0 = c.ge(Jit.FS0, Jit.FR0); c.ret(Jit.FS0); var l1 = c.label(); c.mov(Jit.FR0, Jit.IMM(0.2)); c.sub(Jit.FR0, Jit.FS0, Jit.FR0); c.call(entry1); c.mov(Jit.FS1, Jit.FR0); c.mov(Jit.FR0, Jit.IMM(0.1)); c.sub(Jit.FR0, Jit.FS0, Jit.FR0); c.call(entry1); c.add(Jit.FR0, Jit.FR0, Jit.FS1); c.ret(Jit.FR0); jump0.setLabel(l1); var code = c.generate(); for (var i = 0.1; i < 3.5; i += 0.1) { var tmr = new SystemTimer(); var r = code.frun(i); System.println("[%8.3f] fib(%3.1f) = %.1f" % tmr.elapsed() % i % r); }浮動小数点数の即値は直接比較メソッドで使えるようにしていないので(すればいいんだけど)一旦レジスタに格納して使う必要がある。
frun()
することで Double 値を受け取れる。結果は以下の通り。[ 0.000] fib(0.1) = 0.1 [ 0.000] fib(0.2) = 0.2 [ 0.000] fib(0.3) = 0.3 [ 0.000] fib(0.4) = 0.5 [ 0.000] fib(0.5) = 0.8 [ 0.000] fib(0.6) = 1.3 [ 0.000] fib(0.7) = 2.1 [ 0.000] fib(0.8) = 3.4 [ 0.000] fib(0.9) = 5.5 [ 0.000] fib(1.0) = 8.9 [ 0.000] fib(1.1) = 14.4 [ 0.000] fib(1.2) = 23.3 [ 0.000] fib(1.3) = 37.7 [ 0.000] fib(1.4) = 61.0 [ 0.000] fib(1.5) = 98.7 [ 0.000] fib(1.6) = 159.7 [ 0.000] fib(1.7) = 258.4 [ 0.000] fib(1.8) = 418.1 [ 0.000] fib(1.9) = 676.5 [ 0.000] fib(2.0) = 1094.6 [ 0.000] fib(2.1) = 1771.1 [ 0.000] fib(2.2) = 2865.7 [ 0.000] fib(2.3) = 4636.8 [ 0.000] fib(2.4) = 7502.5 [ 0.000] fib(2.5) = 12139.3 [ 0.001] fib(2.6) = 19641.8 [ 0.001] fib(2.7) = 31781.1 [ 0.002] fib(2.8) = 51422.9 [ 0.003] fib(2.9) = 83204.0 [ 0.004] fib(3.0) = 134626.9 [ 0.006] fib(3.1) = 217830.9 [ 0.015] fib(3.2) = 352457.8 [ 0.020] fib(3.3) = 570288.7 [ 0.027] fib(3.4) = 922746.5出力コードは以下の通り。これも Windows 版。浮動小数点数を引き渡すために、最初に簡単なフック関数がある。SLJIT は関数のエントリポイントでの引数で浮動小数点数を指定できないので、こういう形で回避している。
そういう意味でも、SLJIT を直接使うよりこっちを使うほうが色々面倒見てくれて良い。ローカル変数領域で必要なサイズを自動的に計算したり、非破壊レジスタのための一時保存コードなども必要な数行うように自動で計算もさせているので。
0: 53 push rbx 1: 56 push rsi 2: 57 push rdi 3: 48 8b d9 mov rbx, rcx 6: 48 8b f2 mov rsi, rdx 9: 49 8b f8 mov rdi, r8 c: 4c 8b 4c 24 d0 mov r9, [rsp-0x30] 11: 48 83 ec 30 sub rsp, 0x30 15: 0f 29 74 24 20 movaps [rsp+0x20], xmm6 1a: f2 0f 10 03 movsd xmm0, qword [rbx] 1e: 48 89 f2 mov rdx, rsi 21: 49 89 f8 mov r8, rdi 24: 48 89 c1 mov rcx, rax 27: e8 0d 00 00 00 call 0x39 2c: 0f 28 74 24 20 movaps xmm6, [rsp+0x20] 31: 48 83 c4 30 add rsp, 0x30 35: 5f pop rdi 36: 5e pop rsi 37: 5b pop rbx 38: c3 ret 39: 53 push rbx 3a: 56 push rsi 3b: 57 push rdi 3c: 48 8b d9 mov rbx, rcx 3f: 48 8b f2 mov rsi, rdx 42: 49 8b f8 mov rdi, r8 45: 4c 8b 4c 24 b0 mov r9, [rsp-0x50] 4a: 48 83 ec 50 sub rsp, 0x50 4e: 0f 29 74 24 20 movaps [rsp+0x20], xmm6 53: f2 0f 11 6c 24 38 movsd [rsp+0x38], xmm5 59: f2 0f 10 f0 movsd xmm6, xmm0 5d: 49 b9 33 33 33 33 33 33 d3 3f mov r9, 0x3fd3333333333333 67: 4c 89 4c 24 40 mov [rsp+0x40], r9 6c: f2 0f 10 44 24 40 movsd xmm0, qword [rsp+0x40] 72: 66 0f 2e f0 ucomisd xmm6, xmm0 76: 73 17 jae 0x8f 78: f2 0f 10 c6 movsd xmm0, xmm6 7c: f2 0f 10 6c 24 38 movsd xmm5, qword [rsp+0x38] 82: 0f 28 74 24 20 movaps xmm6, [rsp+0x20] 87: 48 83 c4 50 add rsp, 0x50 8b: 5f pop rdi 8c: 5e pop rsi 8d: 5b pop rbx 8e: c3 ret 8f: 49 b9 9a 99 99 99 99 99 c9 3f mov r9, 0x3fc999999999999a 99: 4c 89 4c 24 40 mov [rsp+0x40], r9 9e: f2 0f 10 44 24 40 movsd xmm0, qword [rsp+0x40] a4: f2 0f 10 e6 movsd xmm4, xmm6 a8: f2 0f 5c e0 subsd xmm4, xmm0 ac: f2 0f 11 e0 movsd xmm0, xmm4 b0: 48 89 c1 mov rcx, rax b3: e8 81 ff ff ff call 0x39 b8: f2 0f 10 e8 movsd xmm5, xmm0 bc: 49 b9 9a 99 99 99 99 99 b9 3f mov r9, 0x3fb999999999999a c6: 4c 89 4c 24 40 mov [rsp+0x40], r9 cb: f2 0f 10 44 24 40 movsd xmm0, qword [rsp+0x40] d1: f2 0f 10 e6 movsd xmm4, xmm6 d5: f2 0f 5c e0 subsd xmm4, xmm0 d9: f2 0f 11 e0 movsd xmm0, xmm4 dd: 48 89 c1 mov rcx, rax e0: e8 54 ff ff ff call 0x39 e5: f2 0f 58 c5 addsd xmm0, xmm5 e9: f2 0f 10 6c 24 38 movsd xmm5, qword [rsp+0x38] ef: 0f 28 74 24 20 movaps xmm6, [rsp+0x20] f4: 48 83 c4 50 add rsp, 0x50 f8: 5f pop rdi f9: 5e pop rsi fa: 5b pop rbx fb: c3 retおわりに
JIT 面白いですね。これでパーサ・コンビネータとか実装して組み合わせたらたら、ちょっとした JIT 付き言語処理系が作れますね。そういう道を目指してもいいかも。
おそらく想定する使い方としては次の 2 通りでしょうか。
- Kinx のライブラリ作成の際に数値計算等の範囲で JIT 化し、高速化する。
- DSL(ドメイン固有言語)やオレオレ言語のホストとなり、バックエンドの出力に使う。
ではまた。
- 投稿日:2020-06-29T20:41:44+09:00
【Ruby on Rails】レイアウトテンプレートの備忘録
初稿です。不備がありましたらご指摘いただけると幸いです。
Ruby on Railsを勉強中です。ビューファイルでコーディングした内容が、なぜDOCTYPE宣言やHTMLタグで囲ってないのにブラウザできちんと表示されるか不明だったのですが、レイアウトテンプレートを理解してスッキリしたので備忘録として残しておきます。
レイアウトテンプレートとは?
railsでアプリケーションを作成した際に、以下のディレクトリに自動で作成されるファイルです。
app/views/layouts/application.html.erb通常、コントローラーで定義したアクションから、ビューファイルがある場合はそのファイルが呼ばれているように見えますが、実際はレイアウトテンプレートの中に作成したビューファイルが埋め込まれて返しているとのこと。
例)postsというコントローラーを作成した場合
app/controllers/posts_controller.rbposts_controller.rbclass PostsController < ApplicationController def index end endapp/views/posts/index.html.erb
index.html.erb<h1>トップページ</h1>実際、レスポンスとして返しているのは、下記テンプレートファイルの
タグ内にある<%= yield %>に呼び出されたビューファイルindex.html.erbが埋め込まれて返されているとのこと。app/views/layouts/application.html.erb
application.html.erb<!DOCTYPE html> <html> <head> <title>FirstApp</title> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> </head> <body> <%= yield %> </body> </html>
- 投稿日:2020-06-29T20:02:14+09:00
【Rails】画像プレビュー機能の実装
目標
開発環境
・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina前提
下記実装済み。
・Slim導入
・Bootstrap3導入
・Font Awesome導入
・ログイン機能実装
・投稿機能実装実装
1.ビューを編集
users/edit.html.slim/ 追記 = f.file_field :profile_image, class: 'img_field', style: 'display:none;' = attachment_image_tag @user, :profile_image, fallback: 'no-image.png', onClick: "$('.img_field').click()", class: 'center-block img-thumbnail img-responsive img_prev' br【解説】
①
file_field
をdisplay:none
の非表示にし、クラスを付与する。= f.file_field :profile_image, class: 'img_field', style: 'display:none;'②
①
で付与したクラスのHTML(file_field)がクリックされたらJavaScriptの処理を実行する。onClick: "$('.img_field').click()"2.
appliacation.scss
を編集appliacation.scss// 追記 .img_prev:hover { cursor: pointer; opacity: 0.7; transform: scale(1.01, 1.01); }【解説】
① 画像にマウスが乗っている時にCSSを反映させる。
.img_prev:hover {}② マウスカーソルをポインターに変更する。
cursor: pointer;③ 不透明度を低くして、画像を少し白くする。
opacity: 0.7;④ 少しだけ画像を拡大する。
transform: scale(1.01, 1.01);3.JavaScriptファイルを作成・編集
ターミナル$ touch app/assets/javascripts/image_preview.jsimage_preview.js$(function () { function readURL(input) { if (input.files && input.files[0]) { var reader = new FileReader(); reader.onload = function (e) { $('.img_prev').attr('src', e.target.result); } reader.readAsDataURL(input.files[0]); } } $('.img_field').change(function () { readURL(this); }); });【解説】
①
1.ビューを編集の②
で実行する処理。$('.img_field').change(function () { readURL(this); });
- 投稿日:2020-06-29T19:50:32+09:00
【RSpec】stub_request(:post)でスタブ化したGoogle APIへのリクエストのレスポンスを実際と同一のレスポンス形式にする
タイトルが長い
RailsアプリケーションからGoogle Calendar APIでカレンダー上のイベントに対して更新処理を行い、そのレスポンスから特定の属性を取り出して取り回すような処理を追加した。
レスポンスで受け取ったJSONをgoogle-api-ruby-clientがGoogle::API::V3::Eventクラスのオブジェクトに変換してしまうため、スタブリクエストのレスポンスもruby-clientで変換させるか同様の構造を持ったオブジェクトにする必要があった。TL;DR
- google-api-ruby-clientを利用してGoogleAPIにリクエストした場合、レスポンスはJSON形式で返ってきたあとgoogle-api-ruby-clientで変換される
- stub_requestからの戻り値のヘッダーに
'X-Goog-Upload-Status': 'final'
を加えると実際のAPIからのレスポンスと同様の形式になるスタブ化したリクエストと実際にAPIにリクエストした場合のレスポンスの違い
下記のコードはgoogle-api-ruby-clientを利用してリクエストし、レスポンスからイベントのIDを取得しようとしている
リクエスト
service = Google::Apis::CalendarV3::CalendarService.new service.authorization = access_token response = service.update_event('primary', event_id, body) event_id = response.idこのとき
response
は以下のようになっている。#<Google::Apis::CalendarV3::Event:0x000055c94da58f18 @attachments=[], @attendees=[#<Google::Apis::CalendarV3::EventAttendee:xxxxxxxxxxxxxxxxxxx @email="hoge@gmail.com", @organizer=true, @self=true>], @created=Mon, 01 Jan 2020 00:00:00 +0000, @creator=#<Google::Apis::CalendarV3::Event::Creator:xxxxxxxxxxxxxxxxxxx @email="hoge@gmail.com", @self=true>, @description="", @end=#<Google::Apis::CalendarV3::EventDateTime:xxxxxxxxxxxxxxxxxxx>, @etag="xxxxxxxxxxxxxxxxxxx", @id="abcdefghijklmnopqrstu", @organizer=#<Google::Apis::CalendarV3::Event::Organizer:xxxxxxxxxxxxxxxxxxx @email="hoge@gmail.com">, @start=#<Google::Apis::CalendarV3::EventDateTime:xxxxxxxxxxxxxxxxxxx>, @updated=Mon, 01 Jan 2020 00:00:00 +0000>このため、
response
オブジェクトから ゲッターメソッドのid
で値を取得できる。一方で大抵の場合、RSpec内でAPIレスポンスは下記のように定義されているだろう。
let(:response) do { id: 'abcdefghijklmnopqrstu', start: { date_time: Time.zone.today }, end: { date_time: Time.zone.today }, html_link: 'https://www.google.com/calendar/event?eid=xxxxxxxxxxxxxxxxxxxxxxxxxxxx', i_cal_uid: 'xxxxxxxxxxxxxxxxxxx@google.com', summary: 'event name', updated: Time.zone.today, ... }.to_json endこの場合イベントIDを取得するためには
JSON.parse(response)['id']
のようにする必要があり、response.id
はNoMethodErrorとなる。このJSON形式のレスポンスをEventクラスのオブジェクトとして返させるためには以下のように
'X-Goog-Upload-Status': 'final'
をレスポンスのヘッダーに付与する必要がある。stub_request( :put, url ).to_return( status: 201, body: response, headers: { content_type: 'application/json', 'X-Goog-Upload-Status': 'final' } )google-api-ruby-clientのテストスペック内でもレスポンスに同様のヘッダー情報が付与されている。
たまたま同僚がすぐに記事やGitHubのissueを見つけてくれたおかげで解決したけどそうじゃなかったら3日はハマってた気がする。
元ネタ
- 投稿日:2020-06-29T19:50:32+09:00
【RSpec】stub_request(:post)でスタブ化したGoogle APIへのリクエストのレスポンスを実際と同一の形式にする
タイトルが長い
RailsアプリケーションからGoogle Calendar APIでカレンダー上のイベントに対して更新処理を行い、そのレスポンスから特定の属性を取り出して取り回すような処理を追加した。
レスポンスで受け取ったJSONをgoogle-api-ruby-clientがGoogle::API::V3::Eventクラスのオブジェクトに変換してしまうため、スタブリクエストのレスポンスもruby-clientで変換させるか同様の構造を持ったオブジェクトにする必要があった。TL;DR
- google-api-ruby-clientを利用してGoogleAPIにリクエストした場合、レスポンスはJSON形式で返ってきたあとgoogle-api-ruby-clientで変換される
- stub_requestからの戻り値のヘッダーに
'X-Goog-Upload-Status': 'final'
を加えると実際のAPIからのレスポンスと同様の形式になるスタブ化したリクエストと実際にAPIにリクエストした場合のレスポンスの違い
下記のコードはgoogle-api-ruby-clientを利用してリクエストし、レスポンスからイベントのIDを取得しようとしている
リクエスト
service = Google::Apis::CalendarV3::CalendarService.new service.authorization = access_token response = service.update_event('primary', event_id, body) event_id = response.idこのとき
response
は以下のようになっている。#<Google::Apis::CalendarV3::Event:0x000055c94da58f18 @attachments=[], @attendees=[#<Google::Apis::CalendarV3::EventAttendee:xxxxxxxxxxxxxxxxxxx @email="hoge@gmail.com", @organizer=true, @self=true>], @created=Mon, 01 Jan 2020 00:00:00 +0000, @creator=#<Google::Apis::CalendarV3::Event::Creator:xxxxxxxxxxxxxxxxxxx @email="hoge@gmail.com", @self=true>, @description="", @end=#<Google::Apis::CalendarV3::EventDateTime:xxxxxxxxxxxxxxxxxxx>, @etag="xxxxxxxxxxxxxxxxxxx", @id="abcdefghijklmnopqrstu", @organizer=#<Google::Apis::CalendarV3::Event::Organizer:xxxxxxxxxxxxxxxxxxx @email="hoge@gmail.com">, @start=#<Google::Apis::CalendarV3::EventDateTime:xxxxxxxxxxxxxxxxxxx>, @updated=Mon, 01 Jan 2020 00:00:00 +0000>このため、
response
オブジェクトから ゲッターメソッドのid
で値を取得できる。一方で大抵の場合、RSpec内でAPIレスポンスは下記のように定義されているだろう。
let(:response) do { id: 'abcdefghijklmnopqrstu', start: { date_time: Time.zone.today }, end: { date_time: Time.zone.today }, html_link: 'https://www.google.com/calendar/event?eid=xxxxxxxxxxxxxxxxxxxxxxxxxxxx', i_cal_uid: 'xxxxxxxxxxxxxxxxxxx@google.com', summary: 'event name', updated: Time.zone.today, ... }.to_json endこの場合イベントIDを取得するためには
JSON.parse(response)['id']
のようにする必要があり、response.id
はNoMethodErrorとなる。対応策
このJSON形式のレスポンスをEventクラスのオブジェクトとして返させるためには以下のように
'X-Goog-Upload-Status': 'final'
をレスポンスのヘッダーに付与する必要がある。stub_request( :put, url ).to_return( status: 201, body: response, headers: { content_type: 'application/json', 'X-Goog-Upload-Status': 'final' } )google-api-ruby-clientのテストスペック内でもレスポンスに同様のヘッダー情報が付与されている。
たまたま同僚がすぐに記事やGitHubのissueを見つけてくれたおかげで解決したけどそうじゃなかったら3日はハマってた気がする。
元ネタ
- 投稿日:2020-06-29T18:33:46+09:00
[Rails]seedファイルごとにデータを管理する
某フリマアプリのクローンサイトをチームで作成中です。
その時に、seedファイルの扱いに少し困ったので備忘録として残しておきます。したいこと
商品を出品する時に、「商品の状態」「配送料」..etcなんかは、
データベースに先に値を入れておいて、プルダウンから選択できるようにする実装
1. rake taskの作成
lib/tasks/直下にseed.rakeを作成し、以下を記載
lib/tasks/seed.rakeDir.glob(File.join(Rails.root, 'db', 'seeds', '*.rb')).each do |file| desc "Load the seed data from db/seeds/#{File.basename(file)}. task "db:seed:#{File.basename(file).gsub(/\..+$/, '')}" => :environment do load(file) end end2. それぞれのseedファイルを作成
db/seedsディレクトリを作成し、そこに「データを入れたいモデル名.rb」のファイルを作成
今回はとりあえず、商品の状態を表すCondition.rbを作成db/seeds/Condition.rbconditions = Condition.create([ {condition: "新品・未使用"}, {condition: "未使用に近い"}, {condition: "目立った傷や汚れなし"}, {condition: "やや傷や汚れあり"}, {condition: "傷や汚れあり"}, {condition: "全体的に状態が悪い"}])3. 実行
あとは実行するだけなので、以下のコマンドをターミナルに
bundle exec rake db:seed:conditionが、問題発生・・・
ローカルの環境ならこのままで良かったのですが、本番環境にデプロイする際に
作成したそれぞれのseedファイルの読み込ませ方が分からず、データを反映できませんでした解決法
seeds.rbに以下の記述を追加
seeds.rbrequire "./db/seeds/condition.rb本番環境のターミナルにて下記のコマンドを実行したらきちんと反映しました
require "./db/seeds/condition.rb終わりに
active hashを使えばもうちょっと楽にできたりしたのかな〜なんて思いますが、
納期もあるので一旦この方法で実装しました
他にいい方法があれば教えていただきたいです参考記事
- 投稿日:2020-06-29T18:30:13+09:00
To install the missing version, run `gem install bundler:2.1.4`と出たときの対処法
日々学んだことやぶち当たったエラーについてまとめていきます。
記載に誤りがありましたら、ご指摘していただけると助かります!
いつも他のかたの記事に助けられているので、少しでもお役に立てればと思います。どういうエラーなのか
Gemfile.lockにbundler 2.1.4が指定しているが、そのバージョンのbundlerが見当たらないというエラー。
Traceback (most recent call last): 2: from 1: from #省略 Could not find 'bundler' (2.1.4) required by your /Users/<ユーザー名>/<ルートディレクトリ名>/Gemfile.lock. (Gem::GemNotFoundException) To update to the latest version installed on your system, run `bundle update --bundler`. To install the missing version, run `gem install bundler:2.1.4`どんなときに起こるのか
Rubyのバージョンの変更をしたときなどに起こる。rbenvではRubyのバージョンごとにgemをインストールする必要があるとのこと。
対処法
gem install bundler -v 2.1.4と入力して、bundler 2.1.4を入れたら解決!
参考
https://qiita.com/kodai_0122/items/c4c13e89dd5c4cba1f32
https://qiita.com/YumaInaura/items/64e5721549e4927ce85f
https://k-koh.hatenablog.com/entry/2020/01/27/200511
- 投稿日:2020-06-29T17:10:02+09:00
bcrypt導入時のエラーたち
bcrypt導入時のエラー
パスワードを暗号化するためのgem, bcryptを導入する際に起こった、小さなエラーの解決法まとめです
その1 An error occurred while installing bcrypt, and Bundler cannot continue.
Gemfilegem 'bcrypt'と書き足し、bundle install を実行したところ、
An error occurred while installing bcrypt (3.1.13), and Bundler
cannot continue.
というエラーが発生。
続けて、エラーメッセージ内に書いてある、
gem install bcrypt -v'3.1.13'
を実行したところ、今度はPermission denied が発生Permissionということはsudoの出番ではないかと下記のコードを実行
$ sudo gem install bcryptこれで無事bundle install 時のエラーが消え、bcryptを追加できました!
その2 cannot load such file -- bcrypt
無事、エラー1を克服し、localhostのブラウザを再起動したにも関わらず、このエラー
そういえば、このような時昔あったなと思い出し、 rails s を再実行。
すると通りました!!
今回はbcrypt導入時の小さいエラーもろもろでした〜
- 投稿日:2020-06-29T08:42:08+09:00
ActiveRecordを使うときは頭にSQLを描こう
みなさん、ActiveRecordを使う時に頭にSQLが浮かんでいますか?
ActiveRecordはとても便利でSQLを意識することなくDBにアクセスしてデータを取得したり、更新したりすることができます。
そのためついつい実装時にSQLを意識せずに書いてしまうことがあります。実装時はActiveRecordを使うことで素のSQLを意識しなくてもよいのですが、最終的に実行する時にはSQLが実行されています。
そのため実際に発行されるSQLをみると、こんなSQL発行されるのと驚くことがあります。SQLを思い浮かべながら書いていたら回避できる実装例をいくつか挙げてみます。
無駄なテーブルをJOINしている
下記のようなモデルがあったとします。
def User < ApplicationRecord has_many :user_organizations end def UserOrganization < ApplicationRecord belongs_to :user belongs_to :organization end def Organization < ApplicationRecord has_many :user_organizations end『organization_idで絞り込んだuserモデルを取得してください。』と言われた時にどのように実装しますか?
target_organization_id = 1 users = User.join(user_organizations: :organization) .where(user_organizations: { organization: { id: target_organization_id })この実装は指定された通りに正しく動作します。発行されるSQLは下記の通り
SELECT `users`.* FROM `users` INNER JOIN `user_organizations` ON `user_organizations`.`user_id` = `users`.`id` INNER JOIN `organizations` ON `organizations`.`id` = `user_organizations`.`organization_id` WHERE `organization`.`id` = 1このSQLをみてどう思いますか?
よく考えるとorganizationsテーブルをJOINしなくてもできることに気づくと思います。
改善後のSQLは下記の通り。SELECT `users`.* FROM `users` INNER JOIN `user_organizations` ON `user_organizations`.`user_id` = `users`.`id` WHERE `user_organizations`.`organization_id` = 1これを実現するActiveRecordは下記の通り。
target_organization_id = 1 users = User.join(:user_organizations) .where(user_organizations: { organization_id: target_organization_id })ActiveRecordのモデル中心に実装を考えると、最初の実装のようについ指定されたidがあるモデルまでJOINしてしまいがちです。
実際にコードレビューをしていてもこのような実装はよく見かけます。
SQLはJOINが少なければ少ないほどパフォーマンスはよくなるので、できる限りJOINが少なくて済むように意識してActiveRecordを実装するようにしましょう。eager_loadのLEFT OUTER JOIN
先ほどと同様に『organization_idで絞り込んだuserモデルを取得してください。』に加えて、後に
user.exam_organization
を使いたいのでキャッシュしておきたい場合、どのように実装しますか?先ほどの実装のままだと、exam_organizationsがキャッシュされていないのでexam_organizationを取得するたびにSQLが発行されてしまい、N+1になってしまいます。
そこで下記のようにjoinsをeager_load(またはincludes)に変更することでキャッシュされるようになります。target_organization_id = 1 users = User.eager_load(:user_organizations) .where(user_organizations: { organization_id: target_organization_id })これで無事キャッシュされるようになるのですが、発行されるSQLをみてみるとINNER JOINがLEFT OUTER JOINに変わってしまっていることに気づきます。
SELECT `users`.id AS t0_r0, ...(全カラム列挙される。長いので省略) FROM `users` LEFT OUTER JOIN `user_organizations` ON `user_organizations`.`user_id` = `users`.`id` WHERE `user_organizations`.`organization_id` = 1SQLを考えずにRailsを書いている場合、INNER JOINで良いところが今回の例のようにLEFT OUTER JOINになっていても気にしないことが多い気がします。
ただSQLが頭に浮かんでいると、必ずデータがある結合なのにLEFT OUTER JOINを選ぶことはあり得ないので違和感しかないです。
このような場合は下記のようにjoinsも追記することでINNER JOINで結合しつつデータをキャッシュすることができます。target_organization_id = 1 users = User.eager_load(:user_organizations).joins(:user_organizations) .where(user_organizations: { organization_id: target_organization_id })最後に
いくつか例を上げてみましたが、どちらもSQLを頭に浮かべながら書いていたら簡単に避けれるようなものばかりです。
多少非効率なことをしていても大抵の場合は問題なく動くので気づかないことが多いですが、積み重ねでパフォーマンスに差が出たりするので、これまでActiveRecordを使う時にSQLを意識してこなかった方も発行されるSQLを意識してみると良いと思います。
- 投稿日:2020-06-29T01:17:43+09:00
[Rails]Ransackでセレクトボックスを使用する方法
はじめに
Ransackで、セレクトボックスを使用する方法についてまとめていきます。
selectについて
まずは、セレクトボックスを作るための
select
ヘルパーについて、説明をしていきます。基本型
select(オブジェクト, プロパティ名, 要素情報, オプション, 要素属性)実装例
<%= f.select :name, [['sample1', 1], ['sample2', 2], ['sample3', 3]], {include_blank: '選択なし'}, class: 'sample' %>(form_forの中に入っているとして、オブジェクトは入れていません。)
このように、第三引数に配列を入れると、パラメータとしてvalueの1や2が送信されます
。便利なヘルパー
さらにセレクトボックスをカスタマイズしていくための
便利なヘルパー
があります。options_for_select
セレクトボックスの初期値
を設定したいときはoptions_for_select
を使用します。基本形
options_for_select(配列/ハッシュ, オプション)実装例
<%= f.select :name, options_for_select({sample1: 1, sample2: 2, sample3: 3}, 1), include_blank: true %>options_for_selectの第2引数に「1」がはいっているので、
1がデフォルト値として表示されます。
options_from_collection_for_select
モデルから選択肢を作成したいときは、
options_from_collection_for_select
を使用すると便利です。基本形
options_from_collection_for_select(オブジェクトの配列, value属性, text項目 , オプション])実装例
<%= f.select :name, options_from_collection_for_select(User.all, :id, :name , 1) %>これで、Userモデルの中にあるnameを自動的にセレクトボックスにすることが出来ます。
Ransackでの使用方法
options_from_collection_for_select
を例として使用します。実装例
<%= search_form_for(@q, url: users_path, local: true) do |f| %> <%= f.select :name_eq, options_from_collection_for_select(User.all, :id, :name , 1) %> <%= f.button ' 検索する' %> <% end %>以上のようにすることで、Userモデル内のnameをセレクトボックスにして、検索することが出来ます。
また、オプションで初期値も設定されています。
name_eq
の_eq
の部分はmatcher
と呼ばれ、等しい値を検索できます。Matcherについて
他にも以下のmatcherがあります。
matcher 意味 _eq 等しい noteq 等しくない _cont 値を含む(LIKE) _iteq 以下 _gteq 以上 詳しくはこちらの「Search Matchers」の項目をご覧ください。
参考
https://railsguides.jp/form_helpers.html
https://shinmedia20.com/rails-select-box
- 投稿日:2020-06-29T00:11:26+09:00
Rubyのクラスメソッドって何?
はじめに
Rubyでつまずいた項目のひとつ、クラスメソッド。
わからなかったことを書いていきます。クラスとインスタンスについてはこちら 。
メソッドとは
前提条件として、メソッドとは何か。
メソッドとは、データが持つ「動作」です。車に当てはめて考えてみると、
車というデータが持つ動作は「走る、止まる、ウィンカーを出す」などです。クラスメソッドとは
クラスメソッドを定義したクラス自身が使えるメソッド。
クラスで共通の情報を持った処理に使うことができます。
定義の方法は他のメソッドと同じですが、メソッド名の前に.selfをつけます。test.rbclass Car def self.run # メソッド名の前にselfをつけてクラスメソッドを定義します puts "走ります" end end Car.run # クラス名.メソッド名で、定義したクラスメソッドを実行できます(2020.6.29追記。ご指摘ありがとうございます)
もしくは class << self から end の間にメソッドを書く記法もあります。
ネストは1段深くなりますが、クラスメソッドをたくさん定義したい場合はメソッド名の前に毎回.selfをつけなくても済みます。test.rbclass Car class << self def run puts "走ります" end end end最後に
クラスメソッドの注意点は以下の通り。
・定義するときには.selfをメソッド名の前につける。
・もしくは class << self から end の間にメソッドを書く記法もある。
・このメソッドは、クラスのみに用いることができます。最後まで読んでいただきまして、ありがとうございました。