Ruby 2.5の新機能

自分が関わったRuby 2.5に導入予定の新機能について書きます。

# Hash#slice

Redmineのチケット: https://bugs.ruby-lang.org/issues/8499

Hash#sliceは、Hashの特定のエントリ(key/value)を抽出するメソッドです。

```ruby
{a: 1, b: 2}.slice(:a) #=> {:a=>1}
```

## ActiveSupport版とRuby 2.5版の挙動の違い

このメソッドはActiveSupportから輸入されたものですが、レシーバがHashのサブクラスである場合の挙動はActiveSupport版とは異なっています。
ActiveSupport版のHash#sliceは、レシーバがHashのサブクラスである場合そのサブクラスを返します。

```ruby:ActiveSupportのHash#slice
require 'active_support'
require 'active_support/core_ext'

class SubHash < Hash; end

sub_hash = SubHash[a: 1, b: 2]
slice = sub_hash.slice(:a) #=> {:a=>1}
slice.class #=> SubHash
```

一方Ruby 2.5で導入されるHash#sliceはレシーバがHashのサブクラスであってもHashを返します。

```ruby:Ruby2.5のHash#slice
class SubHash < Hash; end

sub_hash = SubHash[a: 1, b: 2]
slice = sub_hash.slice(:a) #=> {:a=>1}
slice.class #=> Hash
```

なぜこのようになっているかというと、Rubyとして他のメソッドとの一貫性を重視したためです。
Hashをフィルタするメソッドとして他に#selectや#rejectがありますが、実はこれらのメソッドはHashのサブクラスがレシーバであってもHashを返します。

```ruby
class SubHash < Hash; end

sub_hash = SubHash[a: 1, b: 2]
sub_hash.select {}.class #=> Hash
sub_hash.reject {}.class #=> Hash
```

SubHashを#selectや#rejectしてHashが返ってくるのは違和感があると思いますが、
 #selectや#rejectの挙動を変えるには#sliceの導入とは別の議論が必要なので、現時点では#sliceの挙動はHashを返す方に寄せています。

## Rails側での対応

上記の通りRuby 2.5に導入されるHash#sliceはサブクラスの扱いが異なっているので、Rails側でRuby 2.5に対応するための修正が必要になりましたが、 a_matsuda さんが必要な修正をcommitしてくださいました。

# IO#writeが複数の引数を取るようになった

Redmineのチケット: https://bugs.ruby-lang.org/issues/9323

IO#writeが複数の引数を取るようになりました。
writeに複数の引数が渡された場合、可能であれば[writev(2)](http://man7.org/linux/man-pages/man2/readv.2.html)を利用して渡された引数をアトミックに書き出すよう試みます。

```shell
% strace ruby2.5 -e '$stdout.write("a", "b")'
(略)
writev(1, [{"a", 1}, {"b", 1}], 2)      = 2
(略)
```

## 実はこっそり使われていたwritev

実は、writevはRuby 2.2からこっそり使われていました ([commit](https://github.com/ruby/ruby/commit/bddde8eee32d1926ce6202f2697b78d0cda737f4))。
writeによってIOのバッファが溢れた場合、writevを使ってバッファとバッファに収まりきらなかったStringを同時に書き出そうと試みます。
これにより、バッファが溢れてしまうようなwriteであっても発行するシステムコールは1回で済みます。
2.2以降のRubyがwritevを使う挙動は以下のコードで確かめることができます。

```ruby:write.rb
str = "a" * 4000

File.open("test", "w") do |f|
  3.times do # IOの書き込みバッファは8192バイトなので、3回目のwriteでバッファが溢れる
    f.write(str)
  end
end
```

```shell
% strace ruby write.rb
(略)
writev(7, [{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 8000}, {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 4000}], 2) = 12000
(略)
```

この挙動については[大江戸Ruby会議 05の資料](https://www.slideshare.net/masakimatsushita/rubywritev2)でも解説しています。

## writeに複数の引数を渡した場合の挙動

Ruby 2.5でwriteに複数の引数を渡せるようになっても、通常ファイルの場合の挙動はあまり変わりません。
通常ファイルの場合バッファリングが行われる上に、前節で見たようにバッファが溢れた場合は以前からwritev(2)が使われるようになっているからです。
以下のコードのようにwriteに3つのStringを渡してみても、writev(2)の呼ばれ方は3回writeした場合と変わっていないことがわかります。

```ruby:write.rb
str = "a" * 4000

File.open("test", "w") do |f|
  f.write(str, str, str)
end
```

```shell
% strace ruby2.5 write.rb
writev(7, [{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 8000}, {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 4000}], 2) = 12000
```


挙動が変わるのは主にIOがTTYである場合、同期モードである場合、Socketである場合です。

### TTYの場合

バッファリングされることなく、writev(2)に"a"と"b"が別々に渡されます。

```shell
% strace ruby2.5 -e '$stdout.write("a", "b")'
(略)
writev(1, [{"a", 1}, {"b", 1}], 2)      = 2
(略)
```

### 同期モードの場合

writeに3つのStringを渡していますが、IOが同期モードであるためにバッファリングされることなくwritevに3つの引数が渡されます。

```ruby:write.rb
str = "a" * 4000

File.open("test", "w") do |f|
  f.sync = true
  f.write(str, str, str)
end
```

```shell
% strace ruby2.5 write.rb
(略)
writev(7, [{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 4000}, {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 4000}, {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 4000}], 3) = 12000
(略)
```

## Socketの場合

### UDP

1引数のwriteを2回繰り返した後に2引数のwriteを行なっています。
2引数のwriteではwritevが使われていることがわかります。

```ruby:udp.rb
require "socket"

str = "a" * 100

s = UDPSocket.new
s.connect("10.12.2.211", 1234)
s.write(str)
s.write(str)
s.write(str, str)
```

```shell
% strace ruby2.5 udp.rb
(略)
write(7, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 100) = 100
write(7, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 100) = 100
writev(7, [{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 100}, {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 100}], 2) = 200
(略)
```

tcpdumpで見てみると、writeを2回呼んだ場合はパケットが2つに分かれていますが、2引数でwriteを呼んだ場合はwritevが使われて1つのパケットにまとめられていることがわかります。

```shell
% sudo tcpdump -i ens3 -n udp port 1234
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens3, link-type EN10MB (Ethernet), capture size 262144 bytes
22:49:08.740243 IP 10.12.2.204.36346 > 10.12.2.211.1234: UDP, length 100
22:49:08.740316 IP 10.12.2.204.36346 > 10.12.2.211.1234: UDP, length 100
22:49:08.740340 IP 10.12.2.204.36346 > 10.12.2.211.1234: UDP, length 200
```

### TCP

TCPの場合でも、TCP_NODELAYを有効にしている場合などでは効果が見込めます。
次の例では、writeに複数の引数を渡すことによってパケットが1つにまとめられています。

```ruby:tcp.rb
require "socket"

str = "a" * 100

s = TCPSocket.open("10.12.2.211", 1234)
s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) # TCP_NODELAYを指定
s.write(str)
s.write(str)
s.write(str, str)
```

```shell
% strace ruby2.5 tcp.rb
write(9, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 100) = 100
write(9, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 100) = 100
writev(9, [{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 100}, {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 100}], 2) = 200
```

```shell
% sudo tcpdump -i ens3 -n port 1234
(略)
22:45:28.805352 IP 10.12.2.204.39106 > 10.12.2.211.1234: Flags [P.], seq 1:101, ack 1, win 229, options [nop,nop,TS val 3263095302 ecr 380867644], length 100
22:45:28.805400 IP 10.12.2.204.39106 > 10.12.2.211.1234: Flags [P.], seq 101:201, ack 1, win 229, options [nop,nop,TS val 3263095302 ecr 380867644], length 100
22:45:28.805419 IP 10.12.2.204.39106 > 10.12.2.211.1234: Flags [P.], seq 201:401, ack 1, win 229, options [nop,nop,TS val 3263095302 ecr 380867644], length 200
(略)
```

アプリケーションによっては、writeに複数の引数を渡してパケット数をまとめることでパフォーマンス上のメリットがあるかもしれません。

# IO.copy_streamでcopy_file_range(2)が利用されるようになった

Redmineのチケット: https://bugs.ruby-lang.org/issues/13867

IO.copy_streamは、IO間のコピーを行うメソッドです。

```ruby
IO.copy_stream(src, dst)
```

Ruby 2.4までの実装では、可能であれば[sendfile(2)](http://man7.org/linux/man-pages/man2/sendfile.2.html)を使ったコピーを試み、sendfileが使えなければread/writeを繰り返してコピーを行う実装になっていました。
Ruby 2.5では、可能であればLinux 4.5から導入された[copy_file_range(2)](http://man7.org/linux/man-pages/man2/copy_file_range.2.html)というシステムコールを使ってコピーのオフロードを試みるようになりました。
どのようなオフロードが行われるかはファイルシステムによって異なりますが、現時点ではNFSの場合はサーバーサイドでのコピー、reflinkが利用可能なファイルシステムではreflinkによるオフロードが行われます。