【一人 bugs.ruby Advent Calendar 2021】[Bug #18293] Time.at in master branch was 25% slower then Ruby 3.0【18日目】
一人 bugs.ruby Advent Calendar 2021 18日目の記事になります。
今日は Time.at
のパフォーマンスが遅くなった話です。
[Bug #18293] Time.at in master branch was 25% slower then Ruby 3.0
Time.at
が 3.0.2
と 3.1.0-dev
と比較して 25% 遅くなったというバグ報告です。
チケットに書かれている比較は以下の通り。
# Test code require 'benchmark/ips' Benchmark.ips do |x| x.report('Time.at') { Time.at(0) } end
# Ruby 3.0.2 $ ruby -v time.rb ruby 3.1.0dev (2021-11-08T13:15:21Z master bd2674ad33) [arm64-darwin21] Warming up -------------------------------------- Time.at 614.843k i/100ms Calculating ------------------------------------- Time.at 6.190M (± 0.3%) i/s - 31.357M in 5.065559s
# Ruby 3.0.1-dev $ ruby -v time.rb ruby 3.1.0dev (2021-11-08T13:15:21Z master bd2674ad33) [arm64-darwin21] Warming up -------------------------------------- Time.at 614.843k i/100ms Calculating ------------------------------------- Time.at 6.190M (± 0.3%) i/s - 31.357M in 5.065559s
これは Time.at
の実装を time.c
から timev.rb
に移動させたのが原因らしい。
以下が当時の実装になります。
def self.at(time, subsec = false, unit = :microsecond, in: nil) Primitive.time_s_at(time, subsec, unit, Primitive.arg!(:in)) end
具体的にはその Ruby 実装でキーワード引数を渡していない場合も Primitive.arg!(:in)
が呼ばれてしまっていたのがネックっぽい?
最終的には Primitive.mandatory_only?
という特殊なメソッドが追加されて条件分岐して高速化されました。
def self.at(time, subsec = false, unit = :microsecond, in: nil) if Primitive.mandatory_only? Primitive.time_s_at1(time) else Primitive.time_s_at(time, subsec, unit, Primitive.arg!(:in)) end end
【一人 bugs.ruby Advent Calendar 2021】[Feature #11689] Add methods allow us to get visibility from Method and UnboundMethod object.【17日目】
一人 bugs.ruby Advent Calendar 2021 17日目の記事になります。
今回はメソッドオブジェクトに対してアクセシビリティの情報を取得するメソッドの話です。
[Feature #11689] Add methods allow us to get visibility from Method and UnboundMethod object.
メソッドのアクセシビリティを返す Method#visibility
UnboundMethod#visibility
を追加する提案です。
Method#visibility
はアクセシビリティに応じて :public
:protected
:private
のいずれかが返ってくる想定です。
ユースケースとしては任意のメソッドをラップする場合に元のメソッドと合わせるために使用することができます。
具体的にはこういうケース や以下のケースなど。
class Object def debugging(name) original = instance_method(name) # 提案 # visibility が `:public` `:protected` `:private` を返す method_visibility = original.visibility # 既存の実装だとこう書く必要がある # method_visibility = if private_method_defined?(name) # :private # elsif protected_method_defined?(name) # :protected # else # :public # end define_method(name) { |*args, &block| pp name original.bind(self).call(*args, &block) } # 元のメソッドに合わせる send(method_visibility, name) end end class X def hoge pp 1 + 2 end private :hoge debugging :hoge end # error: private method `hoge' called for #<X:0x000055f0b1abf3e0> (NoMethodError) X.new.hoge
このチケットでは色々と議論が進んでいたんですが最終的には元々の提案の #visibility
は入りませんでした。
その代わりとして {Method,UnboundMethod}#{public?,private?,protected?}
のようにアクセシビリティを判定するメソッドが追加されました。
def hoge end pp method(:hoge).private? # => true pp method(:hoge).public? # => false public :hoge pp method(:hoge).private? # => false pp method(:hoge).public? # => true
最初に提示されたユースケースだとこんな感じで利用することができます。
class Object def debugging(name) original = instance_method(name) # 既存の実装だとこう書く必要がある method_visibility = if original.private? :private elsif original.protected? :protected else :public end define_method(name) { |*args, &block| pp name original.bind(self).call(*args, &block) } # 元のメソッドに合わせる send(method_visibility, name) end end class X def hoge pp 1 + 2 end private :hoge debugging :hoge end # error: private method `hoge' called for #<X:0x000055f0b1abf3e0> (NoMethodError) X.new.hoge
どちらかと判定メソッドが必要なケースが多そうなのでこっちのほうが汎用性は高そうですね。
2021/12/16 今回の気になった bugs.ruby のチケット
今週は {Method,UnboundMethod}#{public?,private?,protected?}
が追加されました。
[Bug #18405] Regression in Struct member setter method parameters
Struct
の最適化をした際にデグレしているというバグ報告
S = Struct.new(:foo) S.instance_method(:foo=).parameters # 最適化前 => [[:req, :_]] # 最適化後 => [[:req]]
- セッターメソッドなのに引数を受け取らないのはおかしいんじゃないか?との事
- 確かに
- これは既に修正済み
[Bug #18408] Rightward assignment into instance variable
- 右代入でインスタンス変数に代入できないのは意図しているのか?というバグ報告
# error: syntax error, unexpected instance variable 42 => @v
- 右代入はパターンマッチの機能を使っているんですがパターンマッチがインスタンス変数などを束縛できないからですね
# error: syntax error, unexpected instance variable case 42 in @v end
[Bug #18396] An unexpected "hash value omission" syntax error when without parentheses call expr follows
- 先週話していた Hash の省略記法で『次の行の式』が値になってしまうというバグ報告の続き
key = "key" # p(key:) ではなくて p(key: 42) になる p key: 42 # => {:key=>42}
- 元々は仕様ということで Reject されていたが matz 的には改善したいらしいので再度 Open された
- https://bugs.ruby-lang.org/issues/18396#note-4
- 動作的には非互換になるが試してみよう、って試みみたい
- コメントでは例えば以下のような Rails DSL を書いていると非互換になるので問題があると書かれている
validate :something, if: -> {condition } # コード長いとここで改行している可能性がある validate :something, if: -> {condition }
[Feature #18384] Pattern Match Object
- パターンマッチを保持するオブジェクトを定義する提案
- 例えば次のように
pattern
でパターンマッチを保持し、ブロック引数に渡すような感じ?
list_of_people.select(&pattern( first_name: /^F/, last_name: /r$/, age: 20..40 ))
Proc
やRegexp
と同じようなイメージ
TARGET_PERSON = PatternMatch.new(first_name: 'something') list_of_people.select(&TARGET_PERSON)
- 同じようなパターンマッチを DRY 的に使い回す場合には便利そう?
- パターンマッチの場合は変数に束縛する機能もあるのでそこをどうするのがいいんだろうか
[Feature #11689] Add methods allow us to get visibility from Method and UnboundMethod object.
- メソッドのアクセシビリティを返す
Method#visibility
UnboundMethod#visibility
を追加する提案:public
:protected
:private
が返ってくる
- ユースケースとしては任意のメソッドをラップする場合に元のメソッドと合わせるために使用できる
class Object def debugging(name) original = instance_method(name) # 提案 # visibility が `:public` `:protected` `:private` を返す method_visibility = original.visibility # 既存の実装だとこう書く必要がある # method_visibility = if private_method_defined?(name) # :private # elsif protected_method_defined?(name) # :protected # else # :public # end define_method(name) { |*args, &block| pp name original.bind(self).call(*args, &block) } # 元のメソッドに合わせる send(method_visibility, name) end end class X def hoge pp 1 + 2 end private :hoge debugging :hoge end # error: private method `hoge' called for #<X:0x000055f0b1abf3e0> (NoMethodError) X.new.hoge
- 元々の提案の
#visibility
は入らなかったが{Method,UnboundMethod}#{public?,private?,protected?}
のように判定するメソッドが追加された
class Object def debugging(name) original = instance_method(name) # 既存の実装だとこう書く必要がある method_visibility = if original.private? :private elsif original.protected? :protected else :public end define_method(name) { |*args, &block| pp name original.bind(self).call(*args, &block) } # 元のメソッドに合わせる send(method_visibility, name) end end class X def hoge pp 1 + 2 end private :hoge debugging :hoge end # error: private method `hoge' called for #<X:0x000055f0b1abf3e0> (NoMethodError) X.new.hoge
- どちらかと判定メソッドが必要なケースが多そうなのでこっちのほうが汎用性は高そうですね
[Feature #18402] Argument Labels
- 次のようにキーワード引数に予約語を使用することはできるが参照する事はむずかしい
def change_color(to:, for:, until:) new_color, user, end_date = to, for, until do_something_with(to) do_something_else_with(for, until) # What does this do with which data again? end change_color(to: :blue, for: user, until: DateTime.tomorrow)
- これを対応するために以下のようにキーワード引数とは別に参照する名前を定義できるようにする提案
def change_color(to new_color:, for user:, until end_date:) do_something_with(new_color) do_something_else_with(user, end_date) # No use of reserved keywords anymore, and readable variable name! end change_color(to: :blue, for: user, until: DateTime.tomorrow)
- これは Swift の『内部引数名と外部引数名』という機能からヒントを得たらしい
- よさそうな気がしつつまたメソッドのシグネチャが複雑になるのか…というお気持ち
- また、以下のように
=>
を使う記法の提案もされている
def change_color(to: => new_color, for: @current_user => user, until: DateTime.tomorrow => end_date)
- ちなみに以下のように Hash の省略記法を使うとスムーズにアクセスができる
def change_color(to:, for:, until:) do_something_with({to:}[:to]) do_something_else_with({for:}[:for], {until:}[:until]) # What does this do with which data again? end change_color(to: :blue, for: user, until: DateTime.tomorrow)
[Feature #18410] Proposal to make inspect include underscores on numerics
964218442
のような数値を表示する際に964_218_442
みたいな表示にする提案- これは便利そうな気がしつつ非互換になってしまうのがちょっと気になる…
- あと単に数値として表示したい場合もあるので必ずしも
_
を付けたいとは限らないんじゃないかなあ - ロケールによって
_
も変える必要があるとコメントされている
【一人 bugs.ruby Advent Calendar 2021】[Bug #17571] prependしたArray#[] が反映されない【16日目】
一人 bugs.ruby Advent Calendar 2021 16日目の記事になります。
今日はArray
に prepend
してる #[]
が呼び出されないバグの話です。
[Bug #17571] prependしたArray#[] が反映されない
以下のように Array
に prepend
してる #[]
が呼び出されないことがあるというバグ報告です。
module TestMod def [](*) :called end end Array.prepend TestMod # 引数がある場合 # これは Array#[] が呼ばれる p [1, 2, 3][1] # => 2 # 引数がない場合 # これは TestMod#[] が呼ばれる p [1, 2, 3][] # => :called # Method オブジェクトは TestMod を指している p [1, 2, 3].method(:[]) # => #<Method: Array(TestMod)#[](*) /tmp/vud3mdg/27:2>
[1, 2, 3][1]
が意図していない挙動になっています。
これは Array#[]
を事前にメソッドキャッシュしており、そちらを優先して呼び出しているのが原因らしいです。
なので prepend
しているメソッドは呼ばれなくなっていた。
更に引数がない場合は Array#[]
とシグネチャが異なるのでキャッシュされたメソッドではなくて prepend
されたメソッドを呼び出しているので意図する挙動になっているようです。
これは Ruby 3.0 に存在しているバグで Ruby 3.1 では修正されています。
【一人 bugs.ruby Advent Calendar 2021】[Misc #18125] A strange behavior when same name variable/method coexist issue.【15日目】
一人 bugs.ruby Advent Calendar 2021 15日目の記事になります。
今日は同名のメソッドと変数を定義した状態の話です。
[Misc #18125] A strange behavior when same name variable/method coexist issue.
次のように同名のメソッドと変数を定義した時に奇妙な動作になるけどこれは期待する動作?という質問です。
def deploy_to "deploy_to" end deploy_to = "#{deploy_to} new place" # ^ これがメソッドではなくて変数を参照している p defined? deploy_to # => local_varible p deploy_to # => " new place"
これは期待する動作でこの場合は変数が定義され値が割り当てられる前のローカル変数 deploy_to
を参照します。
Ruby の場合はメソッドの ()
を省略して記述する事ができるので名前参照がメソッドと変数で曖昧になってしまうんですが基本的には変数を優先して参照します。
def deploy_to "deploy_to" end # これはメソッドを参照する pp deploy_to # => "deploy_to" deploy_to = 42 # 変数が定義された以降であれば変数を参照する pp deploy_to # => 42
なのでチケットに書かれているような deploy_to = "#{deploy_to} new place"
の場合は変数が定義された後に "#{deploy_to} new place"
が評価される流れになるので deploy_to = nil
の状態で式展開が行われます。
こういう時にメソッドを参照したい場合は ()
を付けてメソッド呼び出しだと明示化する必要があります。
def deploy_to "deploy_to" end deploy_to = "#{deploy_to()} new place" # ^ これはメソッド呼び出しになる p deploy_to # => "deploy_to new place"
余談ですが eval
などで動的に Ruby の式を評価すると『変数定義よりも前に処理しても』変数を参照します。
def deploy_to "deploy_to" end # これはメソッドを参照する pp deploy_to # => "deploy_to" # これは変数を参照する # 変数にはまだ価が割り当てられていないので nil を返す pp eval("deploy_to") # => nil deploy_to = 42
binding.irb
などでは内部で eval
を使って式を評価しているのでメソッドと同名の変数を定義する場合は注意しましょう。
【一人 bugs.ruby Advent Calendar 2021】[Feature #17398] SyntaxError in endless method【14日目】
一人 bugs.ruby Advent Calendar 2021 14日目の記事になります。
今日はエンドレスメソッド定義の本体が statement だった場合にエラーになる話です。
[Feature #17398] SyntaxError in endless method
以下のようにエンドレスメソッド定義の本体が statement だった場合にエラーになる、というチケットです。
# OK def foo() = puts("bar") # syntax error, unexpected string literal, expecting `do' or '{' or '(' def hoge() = puts "bar"
これはチケット内でも議論があったのですが最終的には動作するように Ruby 3.1 で対応されました。
# OK def print(value) = puts value print(42) # => 42
ただし、次のように def
の戻り値を他のメソッドに渡す場合はシンタックスエラーになるので注意が必要です。
# syntax error, unexpected local variable or method, expecting `do' or '{' or '(' private def hoge(value) = puts value # () を付けると OK private def foo(value) = puts(value)
【一人 bugs.ruby Advent Calendar 2021】[Feature #16806] Struct#initialize accepts keyword arguments too by default【13日目】
一人 bugs.ruby Advent Calendar 2021 13日目の記事になります。
Struct
の keyword_init:
をデフォルトで有効にするというチケットです。
[Feature #16806] Struct#initialize accepts keyword arguments too by default
Struct
の keyword_init:
をデフォルトで有効化させるチケットです。
対応後は以下のような挙動になる想定です。
User = Struct.new(:name, :age) # これは以前の挙動のまま mami = User.new("mami", 14) pp mami # => #<struct User name="mami", age=14> # キーワード引数を渡すと keyword_init: true と同じように初期化される homu = User.new(name: "homu", age: 14) pp homu # => #<struct User name="homu", age=14>
ただし keyword_init: false
の場合でも User.new(name: "homu", age: 14)
の用に渡せる事ができます。
User = Struct.new(:name, :age, keyword_init: false) # キーワード引数ではなくて User.new({ name: "homu", age: 14 }) と同じ意味になる # なので現状だと name に Hash オブジェクトが入ってしまうので既存の挙動と変わってしまう p User.new(name: "homu", age: 14) # => #<struct User name={:name=>"homu", :age=>14}, age=nil>
デフォルトで keyword_init: true
にした場合に上のようなコードの意味が変わってしまい非互換な変更になってしまいます。
なので Ruby 3.1 ではまず keyword_init: false
に対してキーワード引数を渡すと警告を出すようにし、Ruby 3.2 で変更するような移行パスになります。
User = Struct.new(:name, :age) # Ruby 3.1 では keyword_init: true でない場合にキーワード引数を渡すと警告が出る # warning: Passing only keyword arguments to Struct#initialize will behave differently from Ruby 3.2. Please use a Hash literal like .new({k: v}) instead of .new(k: v). p User.new(name: "homu", age: 14) # => #<struct User name={:name=>"homu", :age=>14}, age=nil> # 明示的に Hash を渡した場合は警告は出ない # no warning p User.new({ name: "homu", age: 14 })
結果的に非互換にはなるんですがこれはよさそうな変更ですね。