しまもん 

モンスターラボ島根開発拠点のブログ

mruby/c(POSIX)で正確な時間計測

はすみきん ・2019-01-01

あけましておめでとうございます。

 

お正月と言えば、傘の上で鞠や熊のプーさんがくるくる転がりますね。 そんなわけなので、mruby/cのループ速度を計測してみました。比較のためCRubyでもやってみました。 ただひたすらに空っぽのループを回して、1ミリ秒あたり何回まわせるのかを計算します。

ソース

# job_loop.rb

if RUBY_VERSION.to_f < 2.1 && MRUBYC_VERSION
  # mruby/cの場合(RUBY_VERSION == "1.9" なのを条件分岐に利用)
  # get_monotonic_arrayメソッドはmain.cに実装されています
  puts 'MRUBYC_VERSION: ' + MRUBYC_VERSION
  class String
    # mruby/cにはrjustが(ljustも)ないので、なんとなく実装します
    def rjust(length, padstr = ' ')
      if self.length < length
        padstr * (length - self.length) + self
      else
        self
      end
    end
  end
else
  # CRubyの場合(バージョン2.1未満では動きません)
  puts 'RUBY_DESCRIPTION: ' + RUBY_DESCRIPTION
  def get_monotonic_array
    # mruby/c版がC言語のclock_gettime()関数を使うので、こちらも同等(たぶん)の実装をしてみます
    nsec_str = Process.clock_gettime(Process::CLOCK_MONOTONIC_RAW, :nanosecond).to_s
    [nsec_str[0..(nsec_str.size - 10)].to_i, nsec_str[(nsec_str.size - 9)..-1].to_i]
  end
end

def get_literal_duration(start, finish)
  start_sec = start[0]
  finish_sec = finish[0]
  start_nsec = start[1]
  finish_nsec = finish[1]
  duration_nsec = if start_nsec > finish_nsec
    # 引き算の繰り下がり
    finish_sec -= 1
    finish_nsec - start_nsec + 1_000_000_000
  else
    finish_nsec - start_nsec
  end
  (finish_sec - start_sec).to_s + '.' + duration_nsec.to_s.rjust(9, '0')
end

def benchmark
  start = get_monotonic_array
  yield
  finish = get_monotonic_array
  puts '  start:  ' + start.to_s + '(sec, nsec)'
  puts '  finish: ' + finish.to_s + '(sec, nsec)'
  return [start, finish]
end

def print_result(res_array, count)
  literal_duration = get_literal_duration(res_array[0], res_array[1])
  puts '  literal duration: ' + literal_duration
  duration = literal_duration.to_f
  puts '  FLOATed duration: ' + duration.to_s
  puts '  ' + (count / duration / 1000).to_s + " loops per 1 millisec"
  puts
end

# この数だけループを回して計測します
#LOOP = 5_000_000
LOOP = 500_000
puts
puts "ループ回数: " + LOOP.to_s
puts

# for inループ
puts "for inループ"
res_array = benchmark do
  for i in 1..LOOP do
    # do nothing
  end
end
print_result(res_array, LOOP)

# timesループ
puts "timesループ"
res_array = benchmark do
  LOOP.times do
    # do nothing
  end
end
print_result(res_array, LOOP)

# whileループ
puts "whileループ"
i = 0
res_array = benchmark do
  while i < LOOP do
    i += 1
  end
end
print_result(res_array, LOOP)

job_loop.rbの大部分はCRubyとmruby/cで共通のスクリプトにするための小細工なのですが、あーでもないこーでもないと書いていたらこっちの方が解説の必要な感じになってしまいました。(プログラムが下手なだけ。。。じつはmruby/cのバグっぽい挙動のせいで変な実装にならざるを得なかった箇所もあります。そこは追々...)

 

で今回、時間を計測するのにC言語の clock_gettime() 関数が返す値をなるべくきちんと使おうとして若干めんどうな実装になりました。 秒セクションとナノ秒セクションを整数のまま別々に計算してからくっつけることでFloat化による誤差を減らそうとしています。 OSが起動してからの時間が長いと誤差が増えるというのが気に入らなかったのです。詳しくはググってください。

 

mruby/cは32bitプロセッサのマイコン向けにつくられているので、Fixnumが32bitなんですね。符号つき4バイト整数ってやつです。int32_t。 表現できる値の範囲は -2,147,483,647 から 2,147,483,647 だと思います。

これも秒セクションとナノ秒セクションを別々に計算した理由のひとつです。 1,234.123456789 秒をナノ秒単位で 1,234,123,456,789 ナノ秒と表現したくても、32bitに収まりません。

 

結局は最後にミリ秒毎のループ回数計算でFloatの割り算をしており、その誤差を考慮したらここでこんなに頑張る必要はないでしょうけど、なんとなく頑張ってみました。 いつか別の場所で役立つかもしれませんし。

ソースのコメントを併せてお読みください。

 

それと、mruby/cにはmain.cも必要です:

#include "mrubyc_src/mrubyc.h"
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "src/job_loop.c"
#define MEMORY_SIZE (1024*40)
static uint8_t my_memory_pool[MEMORY_SIZE];

/*
  時間計測メソッド。単位はナノ秒
*/
static void c_get_monotonic_array(mrb_vm *vm, mrb_value *v, int argc){
  struct timespec ts;
  mrb_value mrb_array = mrbc_array_new(vm, 2);
  clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
  mrb_value sec = mrb_fixnum_value(ts.tv_sec);
  mrb_value nsec = mrb_fixnum_value(ts.tv_nsec);
  mrbc_array_set(&mrb_array, 0, &sec);
  mrbc_array_set(&mrb_array, 1, &nsec);
  SET_RETURN(mrb_array);
}

int main(void) {
  mrbc_init(my_memory_pool, MEMORY_SIZE);
  mrbc_define_method(0, mrbc_class_object, "get_monotonic_array", c_get_monotonic_array);
  mrbc_create_task(job_loop, 0);
  mrbc_run();
  return 0;
}

main.cとjob_loop.rbを統合する方法がわからない場合は、↓の記事を参考にしてみてください。

mruby/cを普通のパソコンで動かす

 

ディレクトリの中身はこんな感じです:

~/work/loop $ tree -L 2
.
├── .gitignore
├── .mrubyc
│   ├── .git
│   (略)
├── .mrubycconfig
├── .ruby-version          # 'mruby-1.4.1'にしてあります
├── Makefile
├── main
├── main.c
├── mrblib
│   ├── .ruby-version     # '2.6.0'にしてあります
│   └── job_loop.rb
├── mrubyc_mrblib
│   ├── Makefile
│   (略)
├── mrubyc_src
│   ├── Doxyfile
│   (略)
└── src
    └── job_loop.c

結果

では結果を見てみましょう。

CRubyの場合

~/work/loop/mrblib $ ruby job_loop.rb
RUBY_DESCRIPTION: ruby 2.6.0p0 (2018-12-25 revision 66547) [x86_64-linux]

ループ回数: 5000000

for inループ
  start:  [22509, 394761284](sec, nsec)
  finish: [22509, 877712311](sec, nsec)
  literal duration: 0.482951027
  FLOATed duration: 0.482951027
  10353.016600997933 loops per 1 millisec

timesループ
  start:  [22509, 883018928](sec, nsec)
  finish: [22510, 263524838](sec, nsec)
  literal duration: 0.380505910
  FLOATed duration: 0.38050591
  13140.400368551436 loops per 1 millisec

whileループ
  start:  [22510, 268057887](sec, nsec)
  finish: [22510, 627531868](sec, nsec)
  literal duration: 0.359473981
  FLOATed duration: 0.359473981
  13909.212527957621 loops per 1 millisec

mruby/cの場合

~/work/loop $ make && ./main
(ビルド中にwarningが出ますけどとりあえずスルー)
MRUBYC_VERSION: 1.2

ループ回数: 5000000

for inループ
  start:  [22150, 39382478](sec, nsec)
  finish: [22156, 436361768](sec, nsec)
  literal duration: 6.396979290
  FLOATed duration: 6.39698
  781.619 loops per 1 millisec

timesループ
  start:  [22156, 436836424](sec, nsec)
  finish: [22162, 212736254](sec, nsec)
  literal duration: 5.775899830
  FLOATed duration: 5.7759
  865.666 loops per 1 millisec

whileループ
  start:  [22162, 212934679](sec, nsec)
  finish: [22164, 283402948](sec, nsec)
  literal duration: 2.070468269
  FLOATed duration: 2.07047
  2414.91 loops per 1 millisec

予想どおり、mruby/cはCRubyよりもだいぶ遅いです。 CRuby、mruby/cともに for in < times < while の順で早くなっていますね。

とくにmruby/cの for intimeswhile より3倍遅いのが目立ちます。 Range#eachとFixnum#timesがRubyで実装されているからです。

このなかを見ればわかります:

https://github.com/mrubyc/mrubyc/tree/master/mrblib

速度を求めないmruby/cの開発方針ってことですね。

 

実行するたびに値は前後しますし、ループ回数を変えると違う結果になるかもしれません。みなさんも試してみてください。

 

以上、いつもより余計に回してみました!(←これを言いたかっただけ)

実験環境

  • マシン:Surface Pro(第5世代) / インテルCore i7
  • OS:VirtualBox上にインストールしたLinux Mint 18.2
~ $ cat /proc/cpuinfo
(略)
model name      : Intel(R) Core(TM) i7-7660U CPU @ 2.50GHz
stepping        : 9
cpu MHz         : 2496.084
cache size      : 4096 KB
(略)

~ $ cat /proc/version
Linux version 4.15.0-39-generic (buildd@lcy01-amd64-012) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.10)) #42~16.04.1-Ubuntu SMP Wed Oct 24 17:09:54 UTC 2018

| 採用情報

城下町・松江で一旗揚げたいRailsエンジニア絶賛募集中!

Blue