デスクトップPCの増強
大抵はMacbookPro (2016年のモデルで2コア4スレッド) で数値計算をしているが,Ryzen5の16コアで殴り始めた知り合いを見て目が覚めた. 力が足りん.
とはいえ新たに自作する余裕もないので2012年にパーツを組み上げて作ったデスクトップPCを増強することにした.
目標
10k以下で8スレッド使える状態を目指した.
デスクトップPCの状態
初めてパーツを買ったあの夏,いろいろ勉強はしていたが,単に自分は組み立てたPCが動けばいいんだということに気づいて,CPUはPentiumで十分だとどこかのパソコンショップの店員に勧められた.2コア2スレッド.確かにネットサーフィンだけなら十分すぎる性能だということが後でわかった. Windowsを買う余裕も無かったのでUbuntuで我慢していた. 幸いメモリは8GB積んでいたし,ちゃんとSSDが入っている. メモリが若干心許ないが,とりあえずCPUの性能向上を第一に考えれば良さそうだった.
- CPU : Pentium G850 (Sandy bridge)
- メモリ : DDR3 1600 4GB $\times$ 2
CPUの選定
8スレッドを目標にしているということで,Sandy bridgeのCore i7をメルカリとヤフオクで探してみたが,どうもそこまで値段が落ちていないことがわかった. 10kじゃ無理かあと思っていたところで,Xeonが目に入った.これだ. Xeonだとグラフィクスが心配だったが,グラフィクス機能がついているのもあるようだった. もしいいのがなければGPUも安いのを買えばいい. あとは目標を達成できるような出品を見つけるだけ.
変数の宣言と定義について
C++の勉強(4)
前回のつづき.変数の「宣言」と「定義」について.
具体的な問題の詳細と正しい実装方法
全てのインスタンスで共通の列挙型変数enum Character {Cheerful, Gloomy}をコンストラクタに与え,それによってインスタンスされたオブジェクトの状態が変わるようにしたかった.この場合には,この列挙型変数によってPersonオブジェクトの性格が決まることになる.そこで,Person.hppで宣言されたクラスPersonをインスタンス化する際に,main.cppに列挙型変数Characterの値を指定したい.これには,Person.hppのコストラクタを次のように定義すれば良い.
Person(Character character):_Char(character){
}
これにより,メンバー変数_Charがコンストラクタに与えられた引数で初期化され,あるPersonオブジェクトの性格が固定される.また,_Charの値によってメンバ関数void Talk_To()の反応が変わるようにPersonクラスを改良すると以下のようになる.
これで,main関数を次のようにする.
//main.cpp #include "Person.hpp" int main(){ Person *Anthony = new Person(Cheerful); std::cout << "Anthony said, " << std::endl; Anthony->Talk_To(); std::cout << "Ben said, " << std::endl; Person *Ben = new Person(Gloomy); Ben->Talk_To(); delete Anthony; delete Ben; return 0; }
コンパイル・実行すると次の結果になる.
Anthony said, Hey bro! Ben said, Ah... Hi...
Personクラスの一つ目のオブジェクトであるAnthonyは性格_Charが明るいCheerful陽キャなので元気に反応するが,Benは暗いGloomyなので反応も暗い.
うまくコンパイルができなかった原因
はじめに,「宣言」と「定義」の違いがわからなかったことが原因でコンパイルできなかったと言ったが,それはenumの仕様を理解していなかったことも原因の一つだった.前回の記事に書いたように,Person.hppの最初にあるenum Character {Cheerful, Gloomy}は「宣言」である.このため,ファイル分割の規則(宣言はヘッダファイルに,定義はソースファイルに)に則ってヘッダファイルに書いている.しかし,使い始めた当初は「宣言」と「定義」の違いがわからずに,とりあえずいろいろなファイルに列挙型変数Characterの存在を知らしめたかったので,extern enum Character {Cheerful, Gloomy}としていた.が,externはある変数の「定義」を外部のファイルにも知らしめるたものもので,「宣言」であるenum ...に対して使えないからコンパイルエラーがでていたのである.これを修正した上のPersonクラスは仕様どおりに正常に動く.
まとめ
enum 型名 {値1, 値2, ...} 変数名で列挙型型名を宣言し,変数名を定義することができる.- ファイル分割の際にはヘッダファイルに宣言を書き,ソースファイルに定義を書くと見通しが良い.
main.cppでAnthony, Benをdeleteしなくてはならなかったのが心苦しかった.
変数の宣言と定義について
C++の勉強(3)
Effective C++とは関係ない話題.ファイル分割と列挙型enumのあたりでコンパイルエラーが頻出したので,そのことについて.
問題の詳細
一番の問題点は,変数の「宣言」と「定義」をよく理解できていなかった点.これを整理したら一発で修正できた.順を追って説明する.
変数の宣言と定義
マイクロソフトの宣言と定義 (C++)を見るのが一番間違いがない.自分でこの二つの言葉について説明しようとしたが,語彙力語りなさすぎて無理だった. 理解していることを簡単にまとめると,そのコードによってデータを伴う (メモリを食う)ような命令が「定義」で,データが伴わない(メモリは消費されない)ような命令を「宣言」と呼ぶと考えると良い.以下に,簡単な例をいくつか示す.
extern bool dec //宣言(externは宣言するためのやつ.) int a; //定義(明示的に初期化されていないが何らかの値がメモリに「保存」されている) int b = 1; //定義(初期化された値がメモリに「保存」されている) double c = 0.1; //定義(型の違いは消費されるメモリのサイズの違いでしかない.定義と宣言の違いで重要なのは,メモリが消費されるか否かなので,サイズの違いは本質的には関係ない.) enum d {d1, d2, d3}; //宣言(列挙型として,新たにdという「型」を作成したと捉える.その型のとる範囲が{d1, d2, d3}のみであることを意味するのがこの宣言の意味.) enum e {e1, e2, e3} e_def; //定義(列挙型としてeという「型」を作成し,e_defという変数を定義した.)
列挙型について
すでに上の例に出したが,c++ではenum 型名 {シンボルの列}で新たな型を「作る」ことができる.さらに,上の例のenum d {d1, d2, d3}という型dは{d1, d2, d3}という値しか取れなくなる.
組み込み型のboolがtrue, falseしかとれないのと同じで,これをenumを用いてわざわざ書いてみると,enum bool {true, false}となる.これと同じノリ.使う分にはこの知識だけあればいける.
より厳密な話は,読みにくいが列挙型 [C++]が確実でしょう.
例にあげたようにこの型の変数を定義するにはenum Type_Name {Symbol0, Symbol1, Symbol2, Symbol4} Variable_Nameのようにしてスペースを3つ挟んだ4つのまとまりが必要.
ファイル分割
クラスの定義が長い場合には,ヘッダファイルとソースファイルを分割することになる.基本的な思想としては,ヘッダファイルには諸々の宣言,ソースファイルには諸々の定義を書けというのがいろいろ調べてくうちに刷り込まれた.例えば,以下の仕様を持つクラスPersonを作ってみる,
* publicなメンバ関数void Talk_To()を持ち,`"Hi."'と返す.
これを満たすクラスのヘッダファイルは次の通り.
//Person.hpp #include <iostream> class Person{ public: Person(); ~Person(); void Talk_To(); };
ソースファイルは次の通り.
//Person.cpp #include "Person.hpp" Person::Person(){} void Person::Talk_To(){ std::cout << "Hi." << std::endl; }
これらから分かるように,ヘッダファイルに書いているのはクラスPersonの宣言.そのメンバ関数も宣言である.一番初めの入門のときに全てのコードを一つのファイルに書いていて,可読性を高めるために関数はプロトタイプ宣言すると良いって書いてある情報がよくある.このプロトタイプ宣言と同様に,ヘッダファイルではmain関数で使われる関数の紹介をしている.で,ソースファイルで行っているのは,Personクラスのメンバ関数の定義である.Personクラス自体はもちろん定義されていない.Personクラスを使用する際には次のmainファイルのようなものからコールする.
//main.cpp #include "Person.hpp" int main(){ Person *P1 = new Person(); //これが定義. P1->Talk_To(); delete P1; return 0; }
結果はHi.のみである.
結局何が言いたいかって言うと,変数の宣言・定義とファイル分割は同値だということ.
分割ファイルのコンパイル
main.cpp,Person.hpp,Person.cppに分割されたファイルをコンパイルするには,まずPerson.cppとmain.cppからオブジェクトファイルPerson.oとmain.oをビルドする.単なるビルドはgccならg++ -c Person.oとすればPerson.oが作られる.が,いまはPerson.cppはPerson.hppをインクルードしているので,オプションとしてPerson.hppのパスをコンパイラに教える必要がある.これには,-I ./とすれば良い.-Iがインクルードファイルのパスを示すためのフラグ.最後に,すべてのオブジェクトファイルをまとめて実行ファイルを作成する.これには,g++ -o Person main.o Person.oとすれば良い.これにより実行ファイルPersonが作成される.
g++ -c Person.cpp -I./g++ -c main.cpp -I./g++ -o Person Person.o main.p
休憩
疲れたし長くなったので次のに分けて書く.
C++の勉強(2)
C++の勉強
Effective C++を読んで参考になったことを書いておく. とりあえず第三章まで読んだ.
コピーコンストラクタについて.
コピーコンストラクタの必要性が全くわからなかった.それはいま書いているコード特有の問題だったので,すっきりした.
サンプルコード
次の機能を持つNumericクラスを考え,main関数での動作も同時に定める.
Numericクラスの仕様
1. int型を保持する動的配列numをメンバ変数とする.インスタンス化の時点でコンストラクタが呼ばれ,メンバ変数lengthが初期化される.さらに,すべての要素が0で初期化される.
2. メンバ関数Set(N)は整数Nをnumのすべての要素に代入する.
3. メンバ関数Print()はnumの0番目の要素を出力する.
4. メンバ関数Compare(N)は,そのオブジェクトのnumの0番目の要素と,別のオブジェクトNのそれを比較して,同じ大きさか違う大きささかを出力する.
このクラスの実装とmain関数で簡単な動作確認を行うためのコードが次の通り.
コピーコンストラクタの意義1
サンプルコードの動作と予想される結果
Numericクラスは一見問題なく動くように見え,その結果は以下の通りになると予想される.
num[0] = 100 num[0] = 50 ちがう大きさ
しかしながら実際に動かしてみると,上記の結果が出力されたあと,
malloc: *** error for object (ポインタが指しているアドレス): pointer being freed was not allocated
と出力されて正常に終了しない.この理由がコピーコンストラクタを定義していないことにある.
なぜうまくいかないか
オブジェクト(ここではインスタンス化されたNumericクラスn1とn2)を関数に渡すとき,すべてのメンバ変数が値渡しされ,その関数のスコープ内でローカルなNumericクラスのオブジェクトnが生成される(これが暗黙に定義されるコピーコンストラクタの定義).そして,ここに落とし穴がある.メンバ変数numはint型のポインタだから,nのメンバ変数numはn2のメンバ変数numで初期化される.すると,その関数が終了し,スコープを外れる時点でオブジェクトnは破棄されるので,nのデストラクタが呼ばれる.この時,n とn2のnumは同じアドレスを指していることから,nの破棄を行うことは,n2のnumをdeleteすることと全く同じ意味になる.したがって,その関数のスコープを外れたあとでn2のnumを用いて行う動作はすべて不定になる.
上で述べたエラーはn2のnumのdeleteがn1->Compare(n2)で行われているのにもかかわらず,main関数のスコープをはずれて,n2が破棄される際にデストラクタが呼ばれたことが原因で生じているのである.
どうしたらよいか
Numericクラスにコピーコンストラクタを定義する.具体的には次の通り.
まずかった原因は,クラスオブジェクトを引数にもつ関数を呼んだ際に,ポインタ変数のコピーのされ方が一致してしまうことなので,単にコピーコンストラクタが呼ばれた際のポインタ配列のコピーのされ方を新たに決めてやればいいという話.ここでは,配列の要素ごとに代入している.この結果,ここで生じたメモリの二重解放エラーはなくなる.
まとめ
ポインタ変数をメンバー変数に持つクラスがあるなら,コピーコンストラクタを定義しておかないと思わぬエラーの原因になる.
C++の勉強(1)方針
C++の勉強
目標
- 公開しても恥ずかしくないようなソースを書く.
現状把握
OpenMPIやOpenMP,Intel MKLなどのライブラリを活用した(共有+分散)メモリ型並列処理による計算結果を思ったとおりに出力することは可能で,効率や安全性を無視して自分のコンピュータ内で仕様する分には問題ない.- クラスの継承やコピーコンストラクタの活用,
constを使うことなど,C++ならではの機能を活用できていない. - クラスが単に「引数が少なく(自分にとっての)可読性と利便性が良い構造体」という意味でしか機能していない.
これまでどのように学習してきた?
CとC++については上・中・入門者で分けるなら入門者の位置づけ.
C
- 学部自体に授業で習ったので,入門書に書いてあるようなことはおおざっぱに理解できている.
C++
- ロベールのC++教室でクラスの章まで読んだ.
どうやって?
- C++ゆとり用 - 藻ログ で紹介されていたドキュメントを一通り見直す.
- cplusplus.comのチュートリアルは見たことがあったが,あ~そういう理由でこうなってるのか!って納得したものがいくつかあったので深くは理解できていなかった.
- 同じく紹介されていたサイボウズの公開記事が目標達成のために役立ちそう.ガチのエンジニアが勉強するための記事だったはず?なので.
- サイボウズの記事で紹介されていたEffective C++を図書館で借りた.さらにmore Effective C++なるものも見つかったのでそちらも.
Julia
Julia のインストール
Pythonで書いていたデータ処理の部分をJuliaで置き換えようと思いたった.Intel MKLを用いてビルドしようとしたが,ハマってしまった上,解決できなかった.
lmkl_rt が見つからんと言われてしまったが,Make.inc では$MKLROOT/lib/intel64/の中でlmkl_rtを探しているらしかったが,$MKLROOT/lib/intel64自体が存在しない.$MKLROOT/lib/libmkl_rt.dylib(動的ライブラリだからそもそもおかしい?)なら見つかったので,これを参照するようにMake.incを書き換えてもだめ.
ググっても同じ問題を述べている人が英語圏含めて見つからなかったので,なにか間違っているのだろうが,疲れたので一旦諦めた.最終的にはおとなしく,v0.6.2とタグ付けされたバージョンをそのままビルドした.
なお,すべての関連ファイルはgit cloneしたjuliaというディレクトリの中だけで完結しているらしいので,アンインストールはそのディレクトリを削除するだけでいいみたい.
julia/usr/bin/Juliaにパスを通したら万事おkかな.