KMC活動ブログ

京大マイコンクラブの活動の様子を紹介します!!

Rustは開発中だけど面白い!【KMCアドベントカレンダー19日目】

あ…これゲームの紹介みたいなタイトルだわ…

この記事は、今年もやります!KMCアドベントカレンダー!! - KMC活動ブログの19日目の記事です。

また、昨日18日目の記事は八ツ橋シューティングについて(前編) 【KMCアドベントカレンダー18日目】 - KMC活動ブログでした。

みなさんこんばんは。KMC二回生のnendokiです。MacBookからMac OS Xを削除するものに鉄槌を下す仕事をしています。

ところで、流行り廃りがあまりにも激しく、あらゆるドキュメントはジンバブエドルのごとくちり紙になることで有名なIT業界ですが、その流れはもっと根本的な、プログラミング言語にまで押し寄せているようですね。

少し前には無数のJS(など)に変換するための言語(いわゆるAltJS)がこの世に産み落とされ、Apple社がSwiftというプログラム言語を発表したことも記憶に新しいかと思います。

今回はタイトルの通りRustというプログラミング言語を紹介します。RustはFirefoxで有名なMozillaが開発主体となっている言語で、2010年ごろ発表だとか2012年にバージョン0.1が公開されたとか(wikipediaより)で割と最近の言語だと思います。印象としてはC++の文法に関数型言語の概念を足してモダンで安全になった言語って感じなので、ビルド環境を整えさえすれば気軽に書き始めることができます。

書き手の時間もないので、Rustの書き方とか機能とかチュートリルとかは該当記事に譲るとして、せめて何が面白いのかなーくらいをつらつら書いていきます。開発中なのでこの仕様はすぐに変わるかもしれません。

参照

Rustの公式ページでは、言語デザインについての哲学についての言及(The Rust Design FAQ)がなされています。

そこにはメモリの安全性は譲れないといった感じの強めの文章が目に飛び込んでくることから、値の扱いについてが言語設計の中心にあることがわかります。実際にプログラマはデータをコピーを減らしつつメモリ上にどう載せるかを考えてプログラムを書くことになります。

特徴的な例として、Rustではポインタは使わない代わりに、nullが許容されない参照を用いることになります。

参照にはlifetimeというラベルのようなものが与えられており、これは値が生成された場所に対して静的に決められます。このlifetimeは関数の返り値とか構造体への格納を行うときに明示する必要があります。逆に言えば、lifetimeのおかげで参照を抱えた構造体といったC++的には微妙極まりないものでも安全に扱うことができるわけですね。

もう一つ、参照には面白い概念があります。それがborrowingと呼ばれるしくみで、大雑把には変数が参照によって参照されている場合、参照元の変数を書き換えることを禁止する仕組みです。参照のスコープを外れることで、参照元の変数は再び書き換え可能になります。この機能は、同じ変数を同時に書き換えたり、使用中に突然値が変わるようなものを防ぐことになるため、マルチプロセスなプログラムを作るときには強力な仕組みとなるでしょう。

box

「lifetimeがあれば参照だって返せるし安全高速だよ✩」と言われても値を作って返す系の関数にはまるで意味がないだろ馬鹿野郎という話になりますね。実際、ポインタを排して参照だけとするとヒープ領域を操作することができません。boxはヒープ領域へのポインタを表す型です。デアドレスも参照と同じ演算子を使います。

このbox、式に対してboxとつけるだけでヒープ上に確保されたことになるお手軽演算子でもあります。余談ですが、古いドキュメントとかでは~演算子が割り当てられたりしています。

boxはヒープ領域へのポインタという書き方は少しだけ正確ではなく、正しくはヒープ領域へのuniqueポインタです。boxを他の変数へコピーすると、元の変数が失効し、参照できなくなります。普通にスコープアウトした場合にメモリを開放すれば良いので、安全に扱うことができそうですね。

ところで、関数へはどのように渡せば良いでしょうか。関数の引数はコピーとして与えられるので、普通に与えたら壊れます。この場合は、boxそのものか一度デアドレスしたものへの参照を与えてやることになります。参照とboxの仕組みが若干ぶつかっている気がしますが、そこは今後に期待ですね。

boxの機能は非常によく使うもののため、幾つかの便利構文があります。その一つにlet boxのように書くことで気軽にunboxできます。以下の例はあまり意味のあるものではありませんが、box内部の値を、boxを破壊することなく取り出しています。box内部への参照が欲しい場合は、let box refと書くことができます。

    let x = box 1u;
    let box y = x;

このbox refという記法はパターンマッチ内でも使うことができるため、boxを含んだenumといったものも簡単に扱えるわけですね。

便利なenum

Cの機能にあったenumとかC++11で新しくなった*1enumとかからさらに進化したenumです。いわゆる関数型プログラミングお馴染みの代数的データ型が採用されています。要するに強い型付けがなされるtagged unionですね。

C++とかでoptional型を作ろうとすると非常に面倒くさいらしいですが、Rustなら以下の通りです。

enum Option<T> {
    None,
    Some(T),
}

Some(T)というのは、T型の値を持つSomeという値になるわけですね。取り出す時にはパターンマッチを使います。

fn is_some<T>(x: &Option<T> ) -> bool {
    match *x {
        Some(_) => true,
        None => false
    }
}

Someの値を使いたいときは_をxとかに置き換えればいいです。コピーコストが怖い場合はref xとか、boxの中身が欲しい場合はbox refとか書くと幸せですね。

ちなみに、Option型は、unsatableながら便利な関数が多いので一度見てみると良いかもしれません。

trait

haskellの型クラスのようなものです(完)。とはいかないので簡単に述べると、未知の型に対して関数を宣言したり実装したりすることができます。オブジェクト指向的に使いたい場合は、構造体を宣言してすぐにその構造体に対してtraitを設定することになります。Javaでいうインターフェースのような使い方をしたい場合は、traitとして適当なインターフェース名にして、他の構造体に対応する実装をimplで個別に与えてやる形になります。

この時、第一引数がself&self&mut selfの場合は、インスタンスメソッドになり、それ以外の場合はクラスメソッドになります。ちなみキャストの関係はself->&self<-&mut selfなので、特にメンバ変数を変更する必要がない場合は&selfで、どうしても変更する必要がある場合は&mut selfだけを実装するのがオススメだそうです。

C++で言う所のtype traitsなのでしょうか。まあ扱いやすくなっていると思います。

まとめ

三つだけかよとお思いの方もいらっしゃるかと思いますが、単純に私のHPが尽きました。チュートリアルにはもっと魅力的な機能がわかりやすく解説されているのでオススメです。

参照だけで十分だとか静的型検査なんて必要ないとのたまう軟弱な言語と比べれば、いかに安全さと速さを求めているかがなんとなくわかるとおもいます。

さて、次回はhatsusatoさんの「C++11 の rvalueの話」だそうです。C++を軽くドついたあとのになってしまい、少し申し訳ないですね。

それでは。

Return Value Optimization (RVO)の話 【KMCアドベントカレンダー20日目】

Return Value Optimization (RVO) の話

この記事は、今年もやります!KMCアドベントカレンダー!! - KMC活動ブログの20日目の記事です。 昨日の19日目の記事はRustは開発中だけど面白い!【KMCアドベントカレンダー19日目】 - KMC活動ブログでした。

KMC3回生のhatsusatoです。 当初はC++のrvalue周りの話をしようと考えていたのですが、rvalueの解説記事は他にたくさんあるしなあ……、と渋っていたら天啓が来たのでRVOの話をします。

RVOとは

Return Value Optimization (RVO)とは、C++におけるコンパイラ最適化技術の名前です。 その名の通り、関数の戻り値に対して余分なコピーや一時オブジェクトを削減する最適化を施します。 コピーコンストラクタには副作用のある処理を書くこともできるので、コピーを削減するRVOがあるのとないのとでは、最終的な実行コードが異なることがあります。 しかし、C++の規格においてRVOは例外的に、たとえ実行コードが変わってしまうとしても行ってよい最適化として認められています。 実際には、コピーやムーブなどにおいてよほど変な処理を書かないかぎり、問題になることはありません。 この機能はよくあるC++コンパイラ(GCCとかClangとかMSVCとか)にはだいたい備わっていて、普段誰もが何気なくお世話になっています。

RVOのしくみ

最適化が適用される主な状況は、関数の戻り値などの一時オブジェクトをreturn文で戻すときです。

#include <iostream>
struct A {
  A(int i = 0) : x(i) {}
  A(const A&) { std::cout << "copy"; }
  int x;
};
A rvo() {
  return A();
}
int main() {
  A a = rvo();  // copy が呼ばれない
  A b = a;  // copy が呼ばれる
  return 0;
}
  • RVOの例

考えてみれば関数(rvo)の戻り値(A())は、その関数の呼び出し元の受け取り先の変数(a)に値を伝えたあとは破棄するだけの一時オブジェクトなので、戻り値を返す(return A())際に直接受け取り先(a)に書き込んでも問題ないはずです。 このような状況において、RVOが実装されているコンパイラでは余分なコピーや一時オブジェクトを削減してくれます。

void rvo(A* _result) {
  new (_result) A();
  return;
}
int main() {
  char a[sizeof(A)];
  rvo(reinterpret_cast<A*>(a));
  reinterpret_cast<A*>(a)->~A();
  return 0;
}

まるでメンバ関数における暗黙のthisポインタのように、関数の引数に戻り値を格納する先の変数へのアドレスを渡します。 そしてそのアドレスの先の上にオブジェクトを構築することで、関数内部での一時オブジェクト生成を呼び出し元のオブジェクト生成とみなすことができます。 このようにしてRVOは実現されています。

さらにNRVO

RVOをさらに推し進めたNamed Return Value Optimization (NRVO)というものがあります。 これはRVOよりも強力な最適化を施しますが、実装されているコンパイラは限られています。 先のRVOではreturn文で戻していたのは一時オブジェクトでしたが、NRVOは名前のついたローカル変数を戻すときに働く最適化です。

A nrvo(int y) {
  A a;
  a.x += y;
  return a;
}
  • NRVOの例

最終的に戻す変数(a)のすべてを、暗黙に受け取った戻り値の格納先(_result)で置き換えることでNRVOを実現しています。

void nrvo(A* _result, int y) {
  new (_result) A();
  _result->x += y;
  return;
}

戻り値として返したいオブジェクトはすべて_resultで表せないと行けないので、すべてのreturn文で戻すオブジェクトが同じオブジェクトでないと最適化が働きません。 同様のことはRVOにも言えます。 同じオブジェクトを返したからと言って最適化が適用されるかどうかはコンパイラによりますが。

A not_nrvo() {
  A a, b;
  return true ? a : b;
}
  • NRVO最適化が適用できない例

まとめ

RVO(およびNRVO)はC++コンパイラにおいて人知れず働き、コードを壊さない範囲でラジカルな最適化を行い、C++のコードを高速化しているのです。

次回予告

明日12月21日(日)のKMCアドベントカレンダーは、 wackyさんによる「イケてると思うdotfilesの管理方法」です。

参考文献