Perl Hackers Hub

第53回Cを用いたPerl拡張入門―Inline::Cで体験してみよう!(3)

(1)こちら⁠2)こちらから。

Perlのスタックを使った引数や戻り値の操作

(3)では、整数値を複数受け取り、その中から最大値、最小値、平均値を求めて返す次のPerlサブルーチンに該当するコードを、Inline::Cを用いて作成します。

my ($min, $max, $avg) = get_score_info(160, 230, 120, 210, 300);
print "min: $min, max: $max, avg: $avg\n";

sub get_score_info {
    my @args = @_; ―(1)
    my $n = @args; ―(2)
    my $min = 0;
    my $max = 0;
    my $sum = 0;
    for (my $i = 0; $i < $n; $i++) {
        $sum += $args[$i]; ―(3)
        if ($i == 0) {
            $min = $args[$i];
            $max = $args[$i];
        } else {
            if ($min > $args[$i]) {
                $min = $args[$i];
            }
            if ($max < $args[$i]) {
                $max = $args[$i];
            }
        }
    }
    my $avg = $sum / $n;
    return ($min, $max, $avg); ―(4)
}

実行結果は次のとおりです。

min: 120, max: 300, avg: 204

上記のコードで注目すべきポイントとして、以下が挙げられます。

  • (1)可変長の引数を受け取ることができる
  • (2)受け取った引数の数を取得する
  • (3)引数の値を1つずつ取得して計算を行う
  • (4)複数値を返す

Perlのサブルーチンは可変長の引数を受け取ることができ、さまざまな値をリストとして返す機能を持っています。これらの操作はPerlの内部ではスタックをベースとして機能しています。

CでPerlの拡張コードを記述する場合、スタックを操作するようなPerlのAPIに触れることになるでしょう。Inline::Cで上記のサブルーチンをCの関数として作成するために、スタック操作について解説していきます。

可変長の引数を受け取る

Inline::C上では可変長の引数であることを、...を用いて表現します。...を用いると引数内で型や変数名を引数へ渡された値と関連付けることができないため、自ら引数のスタックへアクセスして、値の取り出しや型の操作を行わなければなりません。Inline::Cでは引数スタックを操作するためのAPIがCのマクロ[1]として提供されています。

それらのマクロを用いて可変長の引数を操作するソースコードは、次のとおりです。

use Inline C;

get_score_info(160, 230, 120, 210, 300);

__DATA__
__C__
void get_score_info(SV* num, ...) {
  int i, n;
  int min, max;
  double sum, avg;

  Inline_Stack_Vars; ―(1)
  n = Inline_Stack_Items; ―(2)
  for (i = 0; i < n; i++) {
    SV* sv = Inline_Stack_Item(i); ―(3)
    int tmp = (int)SvIV(sv); ―(4)
    sum += (double)tmp;
    if (i == 0) {
      min = tmp;
      max = tmp;
    } else {
      if (min > tmp) {
        min = tmp;
      }
      if (max < tmp) {
        max = tmp;
      }
    }
  }
  avg = sum / (double)n;
  printf("min: %d, max: %d, avg: %.0f\n", min, max, avg);
}

ポイントを見ていきましょう。

  • (1)CでPerlのサブルーチンと同様に可変長の引数を受け取るためにスタックを操作する必要があり、このマクロではその準備を行う。ほかのInline_Stack関連のマクロよりも必ず先に宣言しなければならない
  • (2)可変長の引数の数を返すマクロ。ここでは引数を5つ渡しているので5が返ってくる
  • (3)$_[i]に相当するマクロ。このマクロを用いることで引数へ渡した値に該当するインデックスを指定して値を取り出す
  • (4)(3)で取り出したSV*からIVを取り出し、intへキャストしてtmpへ代入する

最後にminmaxavgの値をprintf関数で出力できます。

複数の値を返す

先ほどの関数をベースに、今度は複数の値を返す関数を定義してみましょう。

CはPerlと違い、1つの値しか返せない言語です。つまりこの場合でも、Inline_Stack関連のマクロを使用し、Perl側へ複数値を返すためにスタックを操作する必要があります。

use Inline C;

my ($min, $max, $avg) = get_score_info(160, 230, 120, 210, 300);
print "min: $min, max: $max, avg: $avg\n";

__DATA__
__C__
void get_score_info(SV* num, ...) {
  int i, n;
  int min, max;
  double sum, avg;

  Inline_Stack_Vars;
  n = Inline_Stack_Items;
  for (i = 0; i < n; i++) {
    SV* sv = Inline_Stack_Item(i);
    int tmp = (int)SvIV(sv);
    sum += (double)tmp;
    if (i == 0) {
      min = tmp;
      max = tmp;
    } else {
      if (min > tmp) {
        min = tmp;
      }
      if (max < tmp) {
        max = tmp;
      }
    }
  }
  avg = sum / (double)n;
  Inline_Stack_Reset; ―(1)
  Inline_Stack_Push(newSViv((IV)min)); ―(2)
  Inline_Stack_Push(newSViv((IV)max));
  Inline_Stack_Push(newSVuv((UV)avg));
  Inline_Stack_Done; ―(3)
}

ポイントを見ていきましょう。

  • (1)宣言することで引数のために使用していたスタックが、戻り値の操作のために使用可能になる
  • (2)戻り値のスタックへ値をpushする
  • (3)複数戻り値を確定させるとき、つまりこれ以上スタックへpushを行わないことを示すために宣言する必要がある

このコードを実行すると次の結果が得られます。

min: 120, max: 300, avg: 204

今回定義したPerlのサブルーチンと同じ実行結果を、Inline::Cでも得ることができました。つまり、PerlのAPIを用いた処理を行うコードをCでも記述できるようになりました。

CでPerlの処理性能を補う

(1)の冒頭にて、Cを用いてPerlを拡張する利点の一つとして、ホットスポットの改善を挙げました。そこではPerlゆえに処理速度が遅くなるケースがあると紹介しましたが、それはいったいどんな処理なのでしょうか。実際にCとPerlのコードを計測、比較を行って調べてみましょう。

Cと比べたPerlの数値計算

PerlとInline::Cそれぞれで記述したaddの処理速度にどれだけ差があるかを計測するために、次のコードを記述します。

計測に用いたPerlのバージョンは5.28.0です。

use Inline C;
use Benchmark 'cmpthese';

my $n = 1;
cmpthese 0, {
    'PP'        => sub {
            pp_add($n, $n);
    },
    'Inline::C' => sub {
            c_add($n, $n);
    },
};

sub pp_add {
    return $_[0] + $_[1];
}
__DATA__
__C__
int c_add(int x, int y) {
    return x + y;
}

計測するために、Benchmarkモジュールのcmptheseを使います。cmptheseを使うと計測の結果がチャート形式で出力されます。

実行結果は次のとおりになりました。

                Rate       PP Inline::C
PP         5434403/s       --      -63%
Inline::C 14530606/s     167%        --

Rateの列は実行速度を表しています。値が大きくなればなるほど1秒で実行できる回数が多くなります。PP[2]とInline::Cの列では、お互いの実行速度を比べてどれくらいの割合の速度なのかを示しています。Cで実行した場合、Perlより約1.7倍も速くなっていることが読み取れます。

Cと比べたPerlの文字列連結

では、文字列を連結する処理はどれくらいの差が生じるでしょうか。計測するために次のコードを記述しました。

use Inline C;
use Benchmark 'cmpthese';

my $a = "Hello";
my $b = ", World";

cmpthese 0, {
    'PP' => sub {
            pp_cat($a, $b);
    },
    'Inline::C' => sub {
            c_cat($a, $b);
    },
};
sub pp_cat {
    return $_[0] . $_[1];
}
__DATA__
__C__
char* c_cat(char* x, char* y) {
    /* 誌面の都合上エラー処理を省略します */
    char* tmp;
    size_t x_len, y_len;
    x_len = strlen(x);
    y_len = strlen(y);
    tmp = (char*)malloc(x_len + y_len); ―(1)
    memcpy(tmp, x, x_len); ―(2)
    memcpy(tmp + x_len, y, y_len); ―(3)
    return tmp;
}

記述したCのコードは次の挙動をします。

  • (1)malloc関数で文字列連結した結果を保存するために、メモリ領域を確保する
  • (2)確保した領域へ変数xに格納された文字列をセットする
  • (3)セットした文字列の最後から続けて、変数yに格納された文字列をセットする

Cでは文字列連結をするためにメモリを操作するコードを記述する必要があります。Perlの場合は内部で複雑なメモリ操作を代わりに行ってくれるので、シンプルに記述ができます。

計測結果は次のとおりになりました。

               Rate Inline::C     PP
PP        5596158/s        9%     --
Inline::C 5134979/s        --     -8%

ほとんど差はありませんが、今度はPerlが上回りました。何度かスクリプトを実行するとCが上回る場合もあります。しかし、どの結果もほぼ同じになるでしょう。

Perlはたった1行のコードでCとほぼ同じ速度で処理ができました。Perlが古くから文字列を処理するための言語だと言われ続けたことに納得できる結果です。

Cで書くべき勘どころ

これらの結果から、数値計算を行う場合、Cで記述するとPerlとほぼ同じ量で記述でき、処理速度が向上することに気付くでしょう。しかし文字列処理を行うコードは、Perlのほうがシンプルに記述できるうえにCとほぼ同等な速度で処理ができます。

PerlとCを比較すると、Perlのほうが直感的に記述でき、より安全に処理を行える利点があります。でもすべての処理を高速に行えるわけではありません。対照的にCで記述すると、高速に処理を行えますが、自由度が高すぎるゆえにメモリを扱う処理ではコードも複雑になります。

簡単にホットスポットを改善したい場合、複雑なメモリ操作を行わない、たくさんの数値計算を行う処理をCで記述するとよいでしょう。実際にたくさんの数値計算を行う例として、前節の最大値、最小値、平均値を求める処理を計測した結果、約11倍の速度向上を図れました。

               Rate        PP Inline::C
PP         335081/s        --      -92%
Inline::C 4186717/s     1149%        --

XSで書かれたPerlモジュール

CPANをより使いやすくしたWebサービスであるMetaCPANでは、XSを用いたモジュールが公開されています。その中から、XSで作成されているからこそ本領発揮しているモジュールを紹介します。

Time::Moment─⁠─XSによる高速化

Time::Momentは、日付や時間周りを操作できるモジュールです。XSで記述されていることから、現在時刻を取得するnow()は同様の機能を持つ組込み関数のlocaltime()より高速に呼び出せます

Encode─⁠─Perlが苦手とするバイト操作のサポート

Encodeは、Perlで扱う文字列のエンコーディングに用いられるモジュールです。モジュール内部ではPerlが苦手とするバイト列の操作を行っています。

JSON::XS─⁠─Perlの型へ高速変換

JSON::XSは、JSONのエンコードやデコードをXSを使って高速に行います。JSONモジュールを利用していれば基本的に裏側でJSON::XSを呼び出してくれますが、もし利用できなかった場合[3]は、Perlだけで記述されたJSON::PPを呼び出します。

autobox─⁠─PerlのAPIを使った構文操作

autoboxは、PerlのAPIをハックすることでPerlの構文をRubyライクに変更できるモジュールです。

参考ドキュメント

もっと深掘りしたい方のために、いくつか読むべきドキュメントを挙げておきます。

perlguts─⁠─Perl APIへの入り口

perlgutsは、Perlインタプリタへ必要な基礎知識について解説しているドキュメントです。Perl APIの使い方も把握できます。

perlxstut─⁠─XSのチュートリアル

perlxstutは、XSチュートリアルのドキュメントです。

perlclib─⁠─PerlでC標準ライブラリをどう扱うか

perlclibは、Cの標準ライブラリを使う際に注意すべき点や、それに代わって使うべきAPIが記述されています。

illguts─⁠─Perl内部の説明書

illgutsは、Perlの内部構造を画像で解説しているPDFドキュメントです。

CによるPerl拡張入門(α)─⁠─日本語によるXSの解説サイト

CによるPerl拡張入門(α)は、XSに関する知見が日本語でとてもわかりやすくまとめられています。

まとめ

今回は、Cを用いてPerlの拡張を行う方法として、Inline::Cを用いた方法を解説しました。Inline::Cを用いたPerlの拡張で得た知識は、今後XSにも挑戦したい方々への助けになるでしょう。ぜひ、Cを用いたPerlの拡張に挑戦してください。

さて、次回の執筆者はわいとんさんで、テーマは「サーバレスでもPerl」です。お楽しみに。

WEB+DB PRESS

本誌最新号をチェック!
WEB+DB PRESS Vol.130

2022年8月24日発売
B5判/168ページ
定価1,628円
(本体1,480円+税10%)
ISBN978-4-297-13000-8

  • 特集1
    イミュータブルデータモデルで始める
    実践データモデリング

    業務の複雑さをシンプルに表現!
  • 特集2
    いまはじめるFlutter
    iOS/Android両対応アプリを開発してみよう
  • 特集3
    作って学ぶWeb3
    ブロックチェーン、スマートコントラクト、NFT

おすすめ記事

記事・ニュース一覧