Perlテックブログ

ITエンジニアの成長意欲を刺激する技術考察、モジュール開発の日記。Perlイベントや国内や海外のPerlの記事の紹介。

実行時になるまで呼び出す関数が存在するかどうかわからない共有ライブラリにおける関数チェック

C言語の関数呼び出しは、すべて静的なものだと思っていた時期が僕にもありました。

でも関数ポインタを使えば、関数を動的に呼び出すことができます。

void* func_address = &foo;
(*func_address)();

コンパイル時に、すべての関数名を静的にチェックすることができれば、それが一番簡単です。リンカが自動的に名前解決してくれます。

実行時に関数のシグネチャをチェックすべき場合とは

実行時に名前をチェックしないと、非常に危険な場合を考えてみましょう。

たとえば、引数がふたつの関数を動的に呼び出して、これをコンパイルしたとしましょ。

でも、その後、その関数の定義が変更されて、引数が一つになったとしましょ。

関数のシグネチャが入れ替わっているのに、コンパイルされたコードでは、関数の引数がひとつだけ渡されます。第二引数は、不正な値になっているでしょう。

こんな場合は、実行時に関数のシグネチャをチェックしないといけませんね。関数と、それ以外では、フィールド呼び出しと、グローバル変数の呼び出しが、名前を持っているので、チェックしないといけませんね。

名前がそもそも変わってしまった場合と、型が変わってしまった場合には、実行時エラーを吐き出すコードを追加しておくと安全です。

パフォーマンスに影響がでるので、関数の先頭部分でまとめてチェックして、一度チェックした場合はキャッシュすることで、パフォーマンス低下の影響を減らすことができそうです。

C言語のソースコードとヘッダファイルとライブラリのパスの仕様化する

C言語は本質的に自由な言語で、ソースコードやヘッダファイルやライブラリをどこに配置するのかということが、決まっていません。

LinuxなどのOSが、どこにソースコードとヘッダファイルとライブラリを配置するかということを、独自にデザインしている。

たとえば、ユーザーが独自にダウンロードしたり、インストールしたものは、次のディレクトリに配置されると想定している。

# ソースファイル
/usr/local/src

# ヘッダーファイル
/usr/local/include

# ライブラリ(静的なもの(.a)と共有ライブラリ(.so)の両方)
/usr/local/lib

C言語におけるコンパイルとリンクの基礎的な概念

ソースコードとか、ヘッダーとか、コンパイルとか、ライブラリとか、ごちゃごちゃしてて、わけわかんない!!!」

ちょっとこれだけ読んどいて!

コンパイラ

C言語において、覚えておいてほしいと思うのは、ライブラリが存在しなくっても、ソースファイルとヘッダファイルでコンパイルはできるということ。

ヘッダには、なんでも書けるのだけれど、うまい設計では、ソースコードから参照する関数名や構造体名やグローバル変数名などの、名前の情報だけが記述されている。実装はなく、完全に名前だけが記述される。

こういう風に設計しておくと、ソースファイルは、ヘッダファイルをインクルードすることで、名前解決を行ってコンパイルをすることができる。

リンカ

リンカは、コンパイルされたソースコードとライブラリを結びつける役割を果たす。

リンカは、定義された名前を元にして、他のコンパイルされたソースコードとライブラリの中から、名前に対応する実装を探す。そして、それを結合する。

Perlにも、Linuxのような標準的なC言語ソースコード配置を持ってきたい

たとえば、以下のような配置で構成したい。それぞれのモジュールのディレクトリの中に標準的なC言語ソースコード配置がある形。


f:id:perlcodesample:20180604072615p:plain

ソースコードでどう関数を並べたらわかりやすい?

ソースコードで関数を書いているときに、「どんな順番で関数を並べたらいいんだろうか」と思ったことはありませんか?

「abc順?」「適当でいい?」「プログラムがわかりやすくなる方法はある?」

僕がPerlC言語ソースコードを書いているときに、こんな風に並べて置いたら、見通しがよくって、書き換えるのもやりやすくなるという方法を紹介するよ。参考にしてね。

呼び出しが深いほうを下の方に書く

僕は最近は、関数の呼び出しがより深い方を、下の方に書くようにしています。たとえば、構文木解析を書いているとき、姉妹ノードを取得するとか、新しいノードを作成するとか、さまざまな場所で、呼び出される、基本的な関数があったりします。

一方でそれを利用して、パッケージやフィールド情報のノードを構築するような関数があります。こちらは、より具体的で、呼び出しが浅いものです。

こんな風にまとめてみました。

姉妹ノード取得 パッケージノード構築
用途 基本的 応用的
呼び出し 深い 浅い
ソースコード位置

基本的で、呼び出しが深くなるものは、ソースコードの下の方に配置、応用的で、呼び出しが浅くなるものは、ソースコードの上の方に配置。

しばらくプログラムを書いていると、基本的な関数は、修正回数がどんどん減っていきます。応用的な関数の修正頻度は、まだまだ高いままです。

修正が少なくなったものは、スクロールがめんどうな下の方へ、修正回数が多いものは、修正しやすい上のほうへ配置する。

僕は、厳密にこういうことをやっているわけではないのですが、ときどき関数位置に迷ったら、こんな感じで、位置を決めています。

ランタイムと配布時のC/C++バインディングの共通化

Perlでは、C言語バインディングをランタイムにおいて行うInline::Cというモジュールがある。

一方で、モジュールを配布するときには、XSという仕組みを利用する。

もし、これらが同じ方法で行えたとしたら便利だと思わないだろうか。

普段のちょっとした用途で、C/C++バインディングしたいときは、インラインで書いて、CPANモジュールとして配布したい場合は、そのコードがそのまま動く、そういう仕組みだ。

ランタイムと配布時のC/C++バインディングを共通化するためには

必要になると考えているものを、ここで列挙してみる。

サブルーチンがC/C++バインディングであるということを指示する

まず、必要なことは、そのサブルーチンが、C/C++バインディングであるということを指示するということだ。これを支持するために、nativeというキーワードを準備する。nativeキーワード、はPerl6におけるC/C++バインディングのキーワードだけれど、僕はこれをPerl 5に持ってこようとしている。

Perl 5は、後方互換性を維持しながら、新しい機能をモジュールにおいて実現できるように設計されている。

こういう設計方針なので、Perl6でPerl5に必要な機能があれば、持ってくることもできるのだ。そして、僕はnativeというキーワードを持ってきた。

package Math {
  # 三角関数
  native sub acos : double ($x : double);
  native sub asin : double($x : double);
  native sub atan : double ($x : double);
  native sub atan2 : double ($y : double, $x : double);
  native sub cos : double ($x : double);
  native sub sin : double ($x : double);
  native sub tan : double ($x : double);
}

こんな風に、sin関数は、C/C++バインディングなんだよということを、nativeキーワードで教えてあげる。

C言語でsin関数を書く

次は、C言語で、sin関数を書く。

#include <math.h>
#include "spvm_native.h"

double SPVM__Math__sin(SPVM_ENV* env, SPVM_VALUE* args) {
  (void)env;
  
  double value = sin(args[0].dval);
  
  return value;
}

共有ライブラリにコンパイル

これをExtUtils::CBuilderを使って共有ライブラリにコンパイル

配布に含めたい場合は、これを、配布ディレクトリに配置する。

ランタイムで実行したい場合は

ランタイムで実行したい場合は、配布に含めないようにしておいて、実行時にコンパイルして、そのまま読み込むようにする。

SPVMで実現したいことは、現在、配布時とランタイム時で、C/C++バインディング方法が異なるので、これを共通化して簡単に利用できるようにしたいというのがあります。

データと関数の組み合わせでオブジェクト指向を実現する

言語の設計思想として、あらゆるものをオブジェクト指向にしようという発想がある。言語の中で扱われるすべてのものが、オブジェクトとなる。

つまり、オブジェクトが先に存在して、そこにメソッドが生えているという考え方だ。

これは、オブジェクトとメソッドが密結合しているともいえる。

これに対して、もう一つのアプローチをとる言語も存在する。

データと関数の組み合わせでオブジェクト指向を実現する

一方で、オブジェクトというあくまで補助的なもので、まず先に、データと関数が存在して、その組み合わせによって、オブジェクト指向を実現しようという言語がある。

この言語の代表は、Perl, Python, Goである。

これらの言語は、関数の第一引数に、自分自身のオブジェクトを表現するものを受け取る。たとえば$selfという表現で受け取る。

sub foo {
  my ($self) = @_;
}

これらは、不完全なオブジェクト指向というとらえられ方をする場合がこれまでは多かったように思う。

でもね、これは非常に大きな利点を持っている。

C言語との相性

PerlPythonC言語で実装されている。C言語は、知っての通り、オブジェクト指向言語じゃない。構造体と関数の集まりだ。PerlPythonオブジェクト指向ができるけれど、実は、C言語の構造体と関数の集まりをうまく利用して、オブジェクト指向を実現している。C言語の表現を完全に覆ってしまうんじゃなくって、うまく利用して、薄いラッパーをかぶせるような感じで、オブジェクト指向を実現する。

PerlPythonから受け取ったのは「self」という変数名だ。そして、Perlはリファレンスとblessを実装して、「$self->foo」のようなメソッド呼び出しのシンタックスシュガーを作って、オブジェクト指向を表現した。

Perlオブジェクト指向

以下の機能をうまく組み合わせて、Perlオブジェクト指向プログラミングをサポートした。

  • ハッシュ
  • リファレンス
  • パッケージ
  • bless
  • サブルーチン
  • メソッド呼び出し

PerlPythonC言語のラッパーと考えるならば、根本的な仕組みがC言語に近いほうが、親和性が高い。

ユーザーがやりたいことはテキスト処理

ユーザーがやりたいことは、ちょこっとしたログを解析したり、試験データを作ったり、集計したりということで、目的はオブジェクト指向というわけじゃない。

もし、オブジェクト指向を知らなくっても、それが、実現できるのであれば、それでもよいとは思う。オブジェクト指向は、ちょいと難しいし。

やりたいことから始まって、必要になったときに、オブジェクト指向を勉強してもいい。

教条的に、言語全体がオブジェクト指向になっていることに、こだわらなくってもよいと思うんだ。

ダイナミックリンクライブラリの呼び出しが遅くなる理由

最近は、ダイナミックリンクライブラリの呼び出しが遅くなる理由はなぜかということについて考えていた。ダイナミックリンクライブラリというのは、実行時にLinuxであれば「.so」Windowsであれば「.dll」で、呼び出しとは、このファイルの中に記述された関数を呼び出すことをいう。

実行時の名前解決

呼び出しが遅くなる主な理由は、呼び出す関数のアドレスが、実行時に決まるからだ。

まず、最初に名前解決を行わないといけない。これは、関数の名前から、関数のアドレスを取得することをいう。関数のアドレスを実行時に取得するということは、関数のアドレスが、実行時になるまで決まらないということだ。

これの何がパフォーマンス上で問題となるか。

コンパイル時のインライン展開が行えないということ

もっとも問題なのは、実行時にアドレス解決するので、コンパイル時には呼び出す関数のアドレスがわからないということだ。これは、コンパイル時における最適化がかからないということを意味する。

コンパイル時の最適化の一つとしてインライン展開がある。これがきかない。 もしループの中で、関数で記述された小さなロジックを繰り返し実行するのであれば、インライン展開がきかないというのは、非常にパフォーマンス上に影響を与える。

たとえば、実際は3行程度の処理であるのに、それがダイナミックリンクライブラリで実装されていて、繰り返し関数が呼び出されたりする場合だ。

小さめの処理は静的に実装するしかない

こういうわけで、大きめの処理であれば、ダイナミックリンクライブラリで十分だけれど、小さめの処理は、インライン展開の恩恵にあづかるために、静的に実装するしかないという結論に至る。

ひとつのファイルの中であれば最適化はきく

ライブラリ間では最適化はきかないけれど、ひとつのファイルの中であれば最適化はきく。なぜなら、一つのファイルの中における、関数は互いに、自分自分について知っているから。

SPVMで数学関数を最適化する問題について考えていて、Math.soに固めていたのだけれど、「これって、インライン展開できなくね!?」と気づく。数学関数でパフォーマンス劣化を起こさせないためには、静的にSPVM自体に関数定義を含めるしかない。そうするとインライン展開をきかすことができる。

Perlはオペレーションシステムと密結合している言語

Perlテックブログに今日、名称が変わりました。

PerlPerlらしくしているものは何だろう。普段は空気のように意識していないけれど、そういう要素が、たくさんあって、それが積み重なっているはずだ。

最近C言語をやっていて比較して気が付いたのだけれど、Perlはオペレーションシステムと密結合している言語だ。

オペレーションシステムというのはOSのことね。WindowsとかLinuxとかMac OS Xとか。

C言語は、オペレーションシステムと疎結合

C言語をやっていると、言語とOSは疎結合しているということがわかる。それは、ファイルオープンなどの、OSの機能を利用するためには、ライブラリを必ず読み込まないといけないからだ。

#include <stdio>

こうして初めてC言語はOSにアクセスするAPIを利用できる。fopen関数などだ。

Perlは、オペレーションシステムと密結合

Perlの場合は、最初からopen関数を使える。何を意味しているかというとPerlはOSと密結合だということ。

さらに密結合なことに、Perlの標準関数は、実は関数ですらない。open関数は、関数というよりも、オペコードであって、関数として分離されておらず、言語自体と密結合している。

Perlの標準関数呼び出しのパフォーマンスのよさは、関数ではなくって、オペコードだということに起因している。関数呼び出しが発生せず、引数や戻り値がどのようなものであるかによって、最適化がされている。

Perlは言語自体が、OSと密結合している。

利点は、デフォルトでオペレーティングシステムの機能が利用できることと、標準関数呼び出しを最適化できること。

欠点は、オペレーティングシステムと分離した状態で利用できないこと。

プログラミング言語銀の弾丸はないというけれど、どちらかの利点をとると、欠点を受け入れざるを得ないということによる。