【一人 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.at3.0.23.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

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}
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
))
  • ProcRegexp と同じようなイメージ
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.

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)
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日目の記事になります。
今日はArrayprepend してる #[] が呼び出されないバグの話です。

[Bug #17571] prependしたArray#[] が反映されない

以下のように Arrayprepend してる #[] が呼び出されないことがあるというバグ報告です。

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日目の記事になります。
Structkeyword_init: をデフォルトで有効にするというチケットです。

[Feature #16806] Struct#initialize accepts keyword arguments too by default

Structkeyword_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 })

結果的に非互換にはなるんですがこれはよさそうな変更ですね。