challenge 分散関数呼び出し

分散関数呼び出しを実装してください.

呼び出される関数は,定価を整数で,割引率(%)を整数で受け取り,
文字列で「販売価格 ○円(定価○円から○%引き)」を返すものとします.
また,数字は3桁のカンマ区切りにするものとします.

たとえば,pricestring(2000, 20) なら
"販売価格 1,600円 (定価2,000円から20%引き)"
を返します.

関数の呼び出し元と,呼び出される側は,物理的に異なる
サーバに配置できることを条件とします.
呼び出し方法は問いませんが,呼び出し方法に名前がある場合,
それをタグに加えてください.
(XML-RPC,SOAP,CORBA,RMI,など)

また,作成した関数を直列に1万回呼び出して,
実行にかかった時間を測定してください.
測定時は別サーバでなくても構いません.
(なるべく別サーバが望ましいです)

測定環境として,
・サーバとクライアントのCPU・メモリ
・同一サーバ内での実行か別サーバでの実行か
・別サーバの場合,通信経路.(100Mbps Ethernet等)
・言語のバージョン
・ミドルウェアを使用している場合,その名前とバージョン
も併記してください.

1つの言語で複数の分散関数呼び出しの実装方法がある場合,
複数の回答を歓迎します.

出題の意図は,様々な分散呼び出し方法の実装例と,
レスポンス速度の確認にあります.
このお題は沢渡 みかげさんの投稿です。 まったく手を加えないでいい完成度の投稿で本当に助かります。 ありがとうございました。

Posted feedbacks - Flatten

Nested Hidden
同一マシンのCoreDuo1.5GHz で1万回11秒
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// サーバ
using System;
using System.Web.Services;
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class Service : System.Web.Services.WebService
{
  [WebMethod]
  public string Calc(int n, int r) {
    int p = n * (100 - r) / 100;
    return string.Format("販売価格 {0}円 (定価{1}円から{2}%引き)",
    p.ToString("C").Substring(1), n.ToString("C").Substring(1), r);
  }
}

// クライアント
using System;
using System.Diagnostics;
using HowWrite.localhost;
class Program
{
  static void Main()
  {
    Service srv = new Service();
    Stopwatch st = new Stopwatch();
    st.Start();
    string s = "";
    for (int i = 0; i < 10000; ++i) s = srv.Calc(2000, 20);
    Console.WriteLine(s);
    st.Stop();
    Console.WriteLine(st.ElapsedMilliseconds / 1000);
  }
}

まずは一番乗り狙いで、最もナイーブな実装をば。
エラー処理や効率は考えていません。
eval-remotelyはリモートホストでインタプリタを起動して評価式を送り込みます。

実行例:
gosh> (ref (run-sample "scherzo" 1) 1)
"販売価格 1,600円 (定価2,000円から20%引き)"

ベンチマーク (CPU Pen4 2GHz, メモリ2GB, ループバック使用、Gauche 0.8.11_pre1, ミドルウェア無し)
gosh> (time (run-sample "scherzo" 10000) (values))
;(time (run-sample "scherzo" 10000) (values))
; real   5.247
; user   0.460
; sys    0.130
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
;; -*- coding: utf-8 -*-
(use gauche.process)
(use srfi-1)

(define (eval-remotely host exprs)
  (let1 p (run-process '(gosh -E "'read-eval-print-loop #f #f #f values'")
                       :host host :input :pipe :output :pipe)
    (unwind-protect
        (map-in-order (lambda (expr)
                      (write expr (process-input p))
                      (newline (process-input p))
                      (read (process-output p)))
                    exprs)
      (begin (close-output-port (process-input p))
             (process-wait p)))))

;; 実行サンプル
(define (run-sample host repeat)
  (eval-remotely host
                 `((define (pricestring 価格 割引率)
                     (format "販売価格 ~:d円 (定価~:d円から~d%引き)"
                            (round->exact (* 価格 (/. (- 100 割引率) 100)))
                            価格 割引率))
                   ,@(make-list repeat '(pricestring 2000 20)))
                 ))

とりあえずPython使ってCGIサーバを立てて見ました。
http://blueeye.bne.jp:7777/cgi-bin/discount.py?2000&33

↑のアドレスにアクセスすると、次のように帰ってきます。
> 販売価格 1340円(定価2000円から33%引き)

以下の記事を参考にしてHTTPサーバを走らせて、その上で下にあるコードを走らせています。
http://coreblog.org/ats/python-de-cgi

今日一日くらい、鯖を立てておきますので遊びたい方はご自由に。
スレッドプールもないようなHTTP鯖なので、それなりに遅いと思いますが、速度測定はそのうちやります。
(ネットゲームのサーバが動いているので、正確な速度測定は難しいと思います)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# -*- coding: utf-8 -*-
#-------------------------以下サーバー側コード
import sys

if(len(sys.argv) == 2):
    (price,discount) = sys.argv[1].split("&")
    price = int(price)
    discount = int(discount)

    print "Content-Type: text/plain"
    print ""
    print "販売価格 %d円(定価%d円から%d%%引き)" % (int(price * (100.0-discount)/100.0 ), price, discount)
else:
    print "Content-Type: text/plain"
    print ""
    print "引数エラー"


# -*- coding: utf-8 -*-
#------------------------以下呼び出し側コード
import urllib
def pricestring(price, discount):
    return urllib.urlopen("http://blueeye.bne.jp:7777/cgi-bin/discount.py?%d&%d" % (price, discount)).read()

print unicode(pricestring(2000,20), "utf8")

ああ、一番争いに敗れた…
ちょっと追加説明です。

リモートサーバに必要なのは、sshの公開鍵認証でログインできることと、Gaucheがインストールされていることのみです。特別にサーバ側で別コードを走らせる必要はありません。

インターネット越しにベンチマークを取ってみました。ハワイからロサンゼルスにあるサーバを叩くと、1000回呼び出しで82秒でした。


何なんだこの遅さは。
直列で100回実験したら18秒強。
10個のスレッドで、並列に10回ずつ取りに行ったら6.5秒。

多分コンソール周りのPrintに時間食っているのかなぁ。
コンソールを表示しなければ、早くなるのか?
まぁ、裏でネットゲームの鯖が動いているところで、実験ってのが無茶な話ですが。

鯖のPCのスペックはこんな感じ。
Pen4 2.8GHz HT
RAM 512MB
OS XP
so-net ADSL12M線
ネットゲームのサーバプログラムがCPUパワーの80%を常時消費

測定したクライアントPC
sinetに接続されている某大学のPC
Pen4 3.2GHz HT
RAM 2GB
OS XP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# -*- coding: utf-8 -*-
import urllib
import time
import threading

def pricestring(price, discount):
    return urllib.urlopen("http://blueeye.bne.jp:7777/cgi-bin/discount.py?%d&%d" % (price, discount)).read()


t1 = time.time()

for x in xrange(100):
    print unicode(pricestring(x * 1000, 20),"utf8")

t2 = time.time()

class TEST_THREAD(threading.Thread):
    def __init__(self, price, discount):
        threading.Thread.__init__(self)
        self.price = price
        self.discount = discount
    def run(self):
        for x in xrange(10):
            print unicode(pricestring(self.price, self.discount + x),"utf8")

threadList = []
for x in xrange(10):
    threadList.append(TEST_THREAD(x * 1000 , 20))
    threadList[-1].start()

for t in threadList:
    t.join()
       
t3 = time.time()

print "series time",t2 - t1
print "pararells time",t3 - t2

pythonの標準ライブラリに含まれているXML-RPCを
使ってみました。

とはいっても、参考にしたIBMのサイトに乗っているサンプル
そのもののような気がしないでもない。

実は数値のコンマ区切りの方が大変でした。

測定環境
 同一マシン上で実行
 Athlon XP 1700+ 512MB
 python 2.5

実行時間
 画面出力を行う    64秒
 画面出力を行わない  112秒
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#
# server.py
#

# -*- coding: utf-8 -*-

import SimpleXMLRPCServer

class PliceString:
  def plicestring(self, p, d):
    def f(i):
      s = str(i)
      n = len(s)
      return ','.join([s[i<0 and -n or i:i+3] for i in range(n-(n+2)/3*3, n, 3)])
    return '販売価格 %s円 (定価%s円から%d%%引き)' % (f(int(p*(1-d*0.01))), f(p), d)

ps = PliceString()
sv = SimpleXMLRPCServer.SimpleXMLRPCServer(('localhost', 8888))
sv.register_instance(ps)
sv.serve_forever()

#
# client.py
#

# -*- coding: utf-8 -*-

import xmlrpclib
import time

sv = xmlrpclib.ServerProxy('http://localhost:8888/')
t0 = time.time()
for i in range(10000):
  sv.plicestring(2000, 20)
#  print sv.plicestring(2000, 20)
print time.time() - t0

Rubyの分散処理の定番dRuby!
DRB_URIを書き換えることで別サーバでも可能。

まず
ruby 45.rb server
でサーバを立ち上げる。そのあと
ruby 45.rb
でクライアントの実行。

・呼び出し10000回にかかった時間 7.9秒
・サーバとクライアントのCPU・メモリ  Pentium4 2.66GHz / 1GB
・同一サーバ内での実行か別サーバでの実行か  同一サーバ
・言語のバージョン  Ruby 1.8.5
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
require 'drb'
DRB_URI = "druby://localhost:2323"

if ARGV.first == 'server'
  obj = Object.new
  def obj.pricestring(price, cut_rate)
    comma = lambda{|n| n.to_s.reverse.scan(/\d{1,3}/).join(",").reverse}
    "販売価格 %s円 (定価%s円から%d%%引き)" % [comma[price*(100-cut_rate)/100], comma[price], cut_rate]
  end
  DRb.start_service DRB_URI, obj
  DRb.thread.join

else                            # client
  require 'benchmark'
  DRb.start_service
  obj = DRbObject.new(nil, DRB_URI)
  puts obj.pricestring(2000,20)
  puts Benchmark.measure { 10000.times{ obj.pricestring(2000,20)  } }.real
end

.NET Remoting版
CoreDuo 1.5GHz の同一マシンで1万回2.7秒
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// サーバ用
using System;
public class Server : MarshalByRefObject
{
  public string Calc(int n, int r)
  {
    int p = n * (100 - r) / 100;
    return string.Format("販売価格 {0}円 (定価{1}円から{2}%引き)",
    p.ToString("C").Substring(1), n.ToString("C").Substring(1), r);
  }
}
class Program
{
  static void Main()
  {
    System.Runtime.Remoting.Channels.ChannelServices.RegisterChannel(
      new System.Runtime.Remoting.Channels.Tcp.TcpChannel(50000), false);
    System.Runtime.Remoting.RemotingConfiguration
      .RegisterWellKnownServiceType(typeof(Server), "HowWrite",
      System.Runtime.Remoting.WellKnownObjectMode.Singleton);
    System.Console.ReadKey();
  }
}

// クライアント用
using System;
using System.Diagnostics;
class Program
{
  static void Main()
  {
    Server  svr = (Server)Activator.GetObject(typeof(Server), 
      "tcp://localhost:50000/HowWrite");
    Stopwatch st = new Stopwatch();
    st.Start();
    string s = "";
	for (int i = 0; i < 10000; ++i) s = svr.Calc(2000, 20);
    Console.WriteLine(s);
    st.Stop();
    Console.WriteLine(st.ElapsedMilliseconds / 1000.0);
  }
}

うへぇー rum-process で host を指定できたんだぁ
すげぇーっ

RMIの実装です(Java 5ではrmic不要になっているのですね)。

一万回呼び出しにかかった時間:4782 ms
・サーバとクライアントのCPU・メモリ:PowerPC G4 1.67 GHz・メモリ 1GB
・同一サーバ内での実行か別サーバでの実行か:同一サーバ内
・言語のバージョン:Java HotSpot(TM) Client VM build 1.5.0_07-87
・ミドルウェアを使用している場合,その名前とバージョン:使用していない
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
共通のインタフェース(Price.java

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Price extends Remote {
    String priceString(int price, int discunt) throws RemoteException;
}


サーバ側実装(Server.java

import java.rmi.registry.*;
import java.rmi.server.*;

public class Server implements Price {
    public String priceString(int price, int discount) {
        return String.format("販売価格 %,d円(定価 %,d円から %d%%引き)", 
                (int)(double)price * (100 - discount) / 100, price, discount);
    }

    public static void main(String[] args) throws Exception {
        Server obj = new Server();
        Price stub = (Price) UnicastRemoteObject.exportObject(obj, 0);
        Registry  reg = LocateRegistry.getRegistry();
        reg.bind("Price", stub);
    }
}


クライアント側実装(Client.java

import java.rmi.registry.*;

public class Client {
    public static void main(String[] args) throws Exception {
        String host = (args.length < 1)? null : args[0];
        Registry reg = LocateRegistry.getRegistry(host);
        Price stub = (Price) reg.lookup("Price");
        long now = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            stub.priceString(1000, 20);
        }
        System.out.printf("%d ms%n", System.currentTimeMillis() - now);
    }
}

Rinda で実装してみました。
まず、ruby ts.rb でタプルスペースを起動します。タプルスペースは黒板みたいなものです。
次に ruby server.rb でサーバを起動。
最後に ruby client.rb でクライアントを起動します。
server.rb と client.rb の起動順は逆でもokです。
server.rb を複数個起動することもできます。もちろん複数プロセスで処理しますし、途中から起動してもokです。
# でも全然スケールしないですけど。

・10000回の実行結果
  ・サーバ1プロセス: 1分 (CPU 使用率 60%ぐらい)
  ・サーバ2プロセス: 1分 (同上)
   全然スケールしない。。。
・Intel Core Duo 1.66GHz、メモリ1G
・同一サーバ
・ruby 1.8.6 (2007-03-13 patchlevel 0) [i386-mswin32]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
--[ts.rb]----------------------------------------

require 'rinda/tuplespace'

$ts = Rinda::TupleSpace.new
DRb.start_service('druby://:1234', $ts)
puts DRb.uri
DRb.thread.join

-- [server.rb] --------------------------------------

require 'rinda/tuplespace'

class Integer
  def commaize
    to_s.gsub(/(\d)(?=(?:\d\d\d)+(?!\d))/, '\1,')
  end
end

def pricestring(price, discount)
  "販売価格 %s円 (定価%s円から%d%%引き)" % [(price*(100-discount)/100).commaize, price.commaize, discount]
end

DRb.start_service
$ts = DRbObject.new_with_uri('druby://localhost:1234')
loop {
  req = $ts.take([:pricestring, nil, nil])
  price, discount = req[1..2]
  $ts.write([:pricestring_result, price, discount, pricestring(price, discount)])
  print "."
}

-- [client.rb] ---------------------------------------

require 'rinda/tuplespace'
require "benchmark"

DRb.start_service
$ts = DRbObject.new_with_uri('druby://localhost:1234')
puts Benchmark.measure {
  10000.times {
    $ts.write([:pricestring, 2000, 20])
    r = $ts.take([:pricestring_result, 2000, 20, nil])
    print "."
  }
}

ScalaでActorを使って。

そもそも非同期に使うのがメインなものなので、同期でやるなんて初めてでした。!?とreplyを使うと同期でメッセージを送受信できます。

が・・・直列で使うと遅いですね。

  • CPU:Athlon 3000+
  • RAM: 1G
  • 同一ホスト

で21218 msでした。直列に呼び出すなら素直にRMI使ったほうがいいですね。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import scala.actors._
import scala.actors.Actor._
import scala.actors.remote._
import scala.actors.remote.RemoteActor._
import java.io.ByteArrayOutputStream

val PORT = 9010

if(args.length>0 && args(0) == "server") {
//server side

  println("server:start")
  actor {
    alive(PORT)
    register('pricestring, self)
    loop {
      react{ 
        case (price:int, p:int) => 
          val s = new ByteArrayOutputStream
          Console.withOut[unit](s){
            printf("\u8ca9\u58f2\u4fa1\u683c {0}\u5186\uff08\u5b9a\u4fa1{1}\u5186\u304b\u3089{2}%\u5f15\u304d\uff09",
              (price*(1-p.toFloat/100)).ceil, price, p)
          }
          reply(s.toString)
      }
    }
  }

}else {
//client side

  val clientActor = actor {
    val c = select(Node("127.0.0.1", PORT), 'pricestring)
    loop {
      react {
        case r@(i:int,j:int) => reply((c!?r))
      }
    }
  }

  import scala.testing.Benchmark
  val test = new Benchmark {
    def run:unit = {
      var i = -1;while({i= i+1; i<10000}) {
        clientActor !? ((2000, 20))
      }
      return ()
    }
  }
  println(test.runBenchmark(1)(0)+" ms")
  System.exit(0)
}

普通にErlangのプロセス間通信を使って実装してみました.
夏休み中なのでとりあえず自宅マシンで測定.

・Intel(R) Xeon(R) CPU 5130  @ 2.00GHz
・同一サーバ内
・Erlang 5.5.4

で 1.04秒くらいでした.

Erlangのプロセス間通信は関数呼び出しの形になっていないので,
呼び出し側が引数をメッセージで送り,
それを受け取ったサーバが呼び出し側に戻値をメッセージで
送り返す形で実装しています.


サーバ側
> erl -noshell -name server@127.0.0.1 -s rcall_s start


クライアント側
> time erl -noshell -name client@127.0.0.1 -s rcall_c main -s init stop
remote pid <4053.36.0>
ret = bench_ok
1.044240 sec.

real    0m2.352s
user    0m0.908s
sys     0m0.156s
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
==== rcall_s.erl

-module(rcall_s).
-export([start/0, init/0]).

start() ->
  proc_lib:start(?MODULE, init, []),
  ok.

init() ->
  global:register_name(calc, self()),
  proc_lib:init_ack(ok),
  loop().

loop() ->
  receive
    {From, Price, Discount} ->
      From ! calc(Price, Discount)
  end,
  loop().


calc(Price, Discount) ->
  lists:flatten(io_lib:format("販売価格 ~s円(定価~s円から~w%引き)",
    [comma(Price * (100-Discount) div 100), comma(Price), Discount])).


comma([N1, N2, N3, N4 | RestN], Acc) ->
  comma([N4 | RestN], [",", N3, N2, N1 | Acc]);
comma(RestN, Acc) ->
  lists:flatten([lists:reverse(RestN), Acc]).

comma(N) when is_integer(N) ->
  comma(lists:reverse(integer_to_list(N)), []).


==== rcall_c.erl

-module(rcall_c).
-export([main/0, bench/2]).

main() ->
  init(),
  Pid = global:whereis_name(calc),
  io:format("remote pid ~w~n", [Pid]),
  {Time, Value} = timer:tc(?MODULE, bench, [Pid, 10000]),
  io:format("ret = ~w~n~f sec.~n", [Value, Time/1000000]).

init() ->
  net_adm:ping('server@127.0.0.1'),
  global:sync().
  % すぐにglobalnameが反映されないので,
  % pingしたあとsyncしてから利用する

bench(_, 0) ->
  bench_ok;
bench(Pid, N) ->
  % 送信元のPIDを知らせないと送り返せない
  Pid ! {self(), 2000, 20},
  receive
    _RetValue ->
      ok
  end,
  bench(Pid, N-1).

C言語 + libxmlrpc(サーバーは apache 経由のCGI)で実装しました。
サーバーサイドのスペック
* Celeron 1.3GHz
* メモリ1G
* xmlrpc-c-1.11.00
11.285 秒でした

最初はシリアルに問い合わせてみたのですが目も当てられないぐらい遅かったので非同期dで 100発 × 100回 というようにパラレルに問い合わせるようにしました。
あとは apache のチューニングでもう少し早くなりそうです。(prefork だったので)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/* サーバー側 CGIコード */
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<xmlrpc.h>
#include<xmlrpc_cgi.h>

static void int2cstr(int num, char *str)
{
    int i, j;
    char buf[16];
    size_t len;
    snprintf(buf, sizeof(buf), "%d", num);
    len = strlen(buf);
    for(i=j=0; i<len; i++){
        str[j++] = buf[i];
        if(i != len - 1 && (len - i - 1) % 3 == 0) str[j++] = ',';
    }
    str[j] = '\0';
}

static xmlrpc_value *pricestring(xmlrpc_env *env, xmlrpc_value *param_array,
                                 void *user_data)
{
    xmlrpc_int32 price, discount;
    int value;
    char str[256], value_str[16], price_str[16];

    xmlrpc_parse_value(env, param_array, "(ii)", &price, &discount);
    if(env->fault_occurred) return NULL;
    value = price - (price * discount / 100);

    int2cstr(price, price_str);
    int2cstr(value, value_str);
    snprintf(str, sizeof(str),
             "販売価格 %s円 (定価%s円から%d%引き)",
             value_str, price_str, discount);
    return xmlrpc_build_value(env, "s", str);
}

int main (int argc, char **argv)
{
    xmlrpc_cgi_init(XMLRPC_CGI_NO_FLAGS);
    xmlrpc_cgi_add_method_w_doc("pricestring", &pricestring, NULL,
                                "s:ii", "Add two integers.");
    xmlrpc_cgi_process_call();
    xmlrpc_cgi_cleanup();
    return EXIT_SUCCESS;
}

/* クライアント側コード */
#include <stdio.h>
#include <stdlib.h>
#include <xmlrpc.h>
#include <xmlrpc_client.h>

#define XMLRPC_URL "http://192.168.0.13/pricestring.cgi"

static void print_state_name_callback (char *server_url,
                       char *method_name,
                       xmlrpc_value *param_array,
                       void *user_data,
                       xmlrpc_env *env,
                       xmlrpc_value *result)
{
    char *str;
    if(env->fault_occurred) return;
    xmlrpc_parse_value(env, result, "s", &str);
    if(env->fault_occurred) return;
    printf("%s\n", str);
}

int main (int argc, char **argv)
{
    int i, j;
    
    xmlrpc_client_init(XMLRPC_CLIENT_NO_FLAGS, NULL, NULL);
    for(i = 0; i < 100; i++){
        for(j = 0; j < 100; j++){
            xmlrpc_client_call_asynch(XMLRPC_URL, "pricestring",
                                      print_state_name_callback, NULL,
                                      "(ii)",
                                      (xmlrpc_int32)2000, (xmlrpc_int32)20);
        }
        xmlrpc_client_event_loop_finish_asynch();
    }
    xmlrpc_client_cleanup();
    return 0;
}

Gaucheのコードを見て思いついた。
サーバー側の  ghci を ssh で起こす。
自宅のクライアントプログラムからインターネットにあるサーバーへアクセスして
pricestring を 10000回実行、結果の文字列10000行をプログラムで受けとる。 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
-- サーバー側 pricestring.hs

import qualified Data.UTF8 as U
import Data.List

pricestring :: Int -> Int -> String
pricestring x y = "販売価格 "++h++" 円 (定価 "++t++" 円から "++r++" %引き)"
  where  h = comint (x*(100-y) `div` 100)
         t = comint x 
         r = comint y

comint :: Int -> String
comint = reverse . concat . intersperse "," . slice 3 . reverse . show

slice :: Int -> [a] -> [[a]]
slice n = unfoldr phi
   where phi [] = Nothing
         phi xs = Just $ splitAt n xs

-- クライアント側

module Main (main) where

import Data.List
import Data.Char
import qualified Data.UTF8 as U
import System.IO
import System.Process

remote = "foo.example.org"

main :: IO ()
main = do { (i,o,e,p) <- runInteractiveCommand "ssh "++host++" ghci -v0 pricestring.hs"
          ; cs <- hGetContents o
          ; es <- hGetContents e
          ; hPutStrLn i "U.putStr $ unlines $ map (uncurry pricestring) $ replicate 10000 (2000,20)"
          ; hFlush i
          ; hPutStrLn i ":q"
          ; hFlush i
          ; putStr cs
          ; putStr es
          }

loop0 h = do { l <- getLine
            ; if "> " `isSuffixOf` l then return ()
              else loop0 h
            }

--
{-
*Main> :main
販売価格 1,600 円 (定価 2,000 円から 20 %引き)
販売価格 1,600 円 (定価 2,000 円から 20 %引き)
販売価格 1,600 円 (定価 2,000 円から 20 %引き)
...
... 結果が10000行
...
(1.04 secs, 183701028 bytes)
-}

あれ?、コードのインデントが変になっちゃった。

よくよく考えると、このコードだと
以下のような処理がシーケンシャルに
行われるのでスケールしなくて当然ですね。

1. client が [:pricestring, ...] を write
2. client が [:pricestring_result] を take
3. server が [:pricestring, ...] をtake して処理
4. server が [:pricestring_result, ] を write
5. client の [:pricestring_result] の take が帰ってくる
6. 1 にもどる


で、処理を変えて client の [:pricestring] を write する処理と 
[:pricestring_result] を take する処理を
別プロセスにしてみたけど、大して変わらなかった。。。

別CPUのマシンで試してみました。

一万回呼び出しにかかった時間:1606 ms
・サーバとクライアントのCPU・メモリ: Intel Pentium M processor 1.60GHz・メモリ 2GB
・同一サーバ内での実行か別サーバでの実行か:同一サーバ内
・言語のバージョン:Java HotSpot(TM) Client VM build 1.6.0_02-b06
・ミドルウェアを使用している場合,その名前とバージョン:使用していない

直しておきました。

原因は部分的にタブ文字が混ざっていたことでした。
どう書くorgではタブ文字をスペース4個で表示しているので、
8スペースの環境でタブとスペースの混在したコードを作ると
タブの所だけへこむことになります。

測定結果と環境を書きわすれていました.

測定
% time runhaskell rpc.hs 
...10000行の出力...
runhaskell rpc.hs  0.93s user 0.47s system 12% cpu 10.790 total

通信
  下り 8Mbps (best effort) の回線でインターネット接続
サーバー
  CPU    : AMD Sempron 2600+ 1.61 GHz 
  メモリ : 500MB
  言語   : ghc-6.6
クライアント
  CPU    : Intel Pentium M 2.13 GHz
  メモリ : 2GB
  言語   : ghc-6.6.1
ミドルウェア
  ssh

Shiro さんの実装に対抗して速度重視のものを書いてみました。ソケットによって、二つの引数を渡しているだけですが、 RPC ということにしておきます。実験的なコードなので、安全性は考えていません。ソケットを閉じてさえいません。

実行速度はやはり速く、 Celeron 2GHz のマシン(同一)で、 /dev/null  に出力したもので、大体3秒程度でした。実行結果は以下のような感じ。

theo-desktop% cd workspace/doukaku 
theo-desktop% ./rpc-server.scm&
[1] 22494
theo-desktop% ./rpc-client.scm
# 一万行の出力を省略
;(time (dotimes (i *call-times*) (format out "~d ~d\n" (u16vector-ref nv ...
; real  29.516
; user   1.170
; sys    1.100
theo-desktop% 
[1]  + done       ./rpc-server.scm
theo-desktop% ./rpc-server.scm&
[1] 22499
theo-desktop% ./rpc-client.scm>/dev/null 
;(time (dotimes (i *call-times*) (format out "~d ~d\n" (u16vector-ref nv ...
; real   3.434
; user   0.960
; sys    0.840
theo-desktop% 
[1]  + done       ./rpc-server.scm
theo-desktop% 

*サーバとクライアントのCPU・メモリ
  メモリ: 749.1MB
  プロセッサ: Intel Celeron CPU 2.00 GHz

*同一サーバ内での実行か別サーバでの実行か
  同一。

*言語のバージョン
  Gauche scheme interpreter, version 0.8.10 [utf-8,pthreads]

*ミドルウェアを使用している場合,その名前とバージョン
  特にないと思われる。

このプログラムを書くにあたって Gauche (at Lingr) において、びさん、 rui さん、 masa_edw さん、 leque さんらに相談にのっていただきました。どうもありがとう!
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
;; クライアント
#!/usr/bin/env gosh
(use gauche.net)
(use srfi-27)
(use gauche.uvector)

(define *call-times* 10000)

(define (random-integers size maxnum)
  (let1 v (make-u16vector size)
    (dotimes (i size)
      (u16vector-set! v i (random-integer maxnum)))
    v))

(define (main args)
  (let ((nvec (random-integers *call-times* (expt 2 16)))
	(mvec (random-integers *call-times* 100)))
    (call-with-client-socket
	(make-client-socket 'inet "localhost" 3000)
      (lambda (in out)
	(time 
	 (dotimes (i *call-times*)
	   (format out
		   "~d ~d\n"
		   (u16vector-ref nvec i) (u16vector-ref mvec i))
	   (flush out)
	   (print (read-line in))
	   ))))))

;; サーバー

(let* ((server (make-server-socket 'inet 3000 :reuse-addr? #t))
       (sock (socket-accept server))
       (in (socket-input-port sock :buffering :modest))
       (out (socket-output-port sock)))
  (with-ports
   in out #f
   (lambda ()
     (do ((n (read) (read))
          (m (read) (read)))
         ((or (eof-object? n) (eof-object? m)))
       (format out "販売価格 ~,,',,4:d (定価~,,',,4:d円から~d%引き)"
               (ceiling (*. n (/. (-. 100 m) 100)))
               n m)
       (newline)
       (flush)))))