目次: OpenCL
今回は少し話題を変えてpoclとClang/LLVMの関係について説明します。poclはOpenCL C言語をビルドするため内部的にClang/LLVMを呼び出します。ビルドはおおまかに2段階に分かれています。設定次第で多少変わりますが、ざっくり各段階で何をしているのか説明します。まだ話題にしていない関数も出てきますが、その話は追々やっていこうと思います。
最初はclBuildProgram() です。やっていることはOpenCL C -> LLVM IR Bitcode(コンパイル+OpenCLランタイムとのリンク)です。処理に関連する関数と呼び出し関係は下記のとおりです。
clBuildProgram() compile_and_link_program() pocl_llvm_build_program(): プリプロセス、コンパイル(出力LLVM IR Bitcode) link(): OpenCLランタイムとリンク(未定義シンボルの検出) pocl_write_module(): 出力LLVM IR Bitcode
この関数の実行中に3つ一時ファイルが出力されます。本来は消されてしまって残らないファイルもありますが、コードを変更し一時ファイルをあえて残すと、
が作成されます。link() は全てのシンボルを問答無用で追加するのではなくて、OpenCLカーネルコードから参照されているシンボル(=未定義のシンボル)のみを追加します。
次はclEnqueueNDRangeKernel() です。やることはLLVM IR Bitcode -> ターゲットバイナリです。
API名はコンパイルやビルドと関係なさそうに見えますが、poclのホストCPU向け実装を見る限り、NDRangeKernelから起因してLLVM IR Bitcodeからターゲットデバイスのバイナリに変換しています(あと何度も変換しなくて良いように生成したバイナリをキャッシュする)。
処理に関連する関数と呼び出し関係は下記のとおりです。
clEnqueueNDRangeKernel() pocl_command_enqueue() pocl_pthread_submit() pthread_scheduler_push_command(): キューにコマンドを追加する、ワーカースレッドが起床してコマンドを得る start_thread(): ワーカースレッド pocl_pthread_driver_thread() pthread_scheduler_get_work() check_cmd_queue_for_device(): キューからコマンドを得る、コマンドがNDRangeKernelの実行要求だったら、下記を呼ぶ pocl_pthread_prepare_kernel() pocl_check_kernel_dlhandle_cache(): dlopen() pocl_check_kernel_disk_cache(): キャッシュ llvm_codegen(): ターゲットデバイスバイナリ生成 pocl_llvm_codegen(): .so.oバイナリ作成 pocl_invoke_clang(): .soバイナリ作成
独自アクセラレータ向け実装もホストCPU向け実装に習いclEnqueueNDRangeKernel() を起点にバイナリに変換を行います。現状はキャッシュ機構はスキップし、毎回ターゲットバイナリを生成する実装です。このままだと非常に遅いので今後、改善する必要があります。
clEnqueueNDRangeKernel() pocl_command_enqueue() pocl_accel_submit(): ここはターゲットデバイスごとに実装が異なる scheduleCommands() pocl_exec_command() pocl_accel_run(): ここから先はターゲットデバイスごとに実装が異なる llvm_codegen(): ターゲットデバイスバイナリ生成 pocl_llvm_codegen(): .so.oバイナリ作成 pocl_invoke_clang(): .soバイナリ作成
この関数の実行中に2つ一時ファイルが出力されます。本来は消されてしまって残らないファイルもありますが、コードを変更し一時ファイルをあえて残すと、
が作成されます。中間バイナリと最終バイナリの差は、C言語のコンパイルのときに生成する *.oと実行ファイルとの違いと同様に、前者はアドレスが解決されておらず、後者はアドレス解決済みという差がありました。他にも何か違いがあるかもしれません。
第1段階で紹介した通りclBuildProgram() のlink() にてOpenCLカーネルに必要なシンボルが集められます。そのため中間バイナリと最終バイナリを比較しても、含まれる関数やシンボルはほぼ変わりません(例外はclang_rt.builtinsに依存した関数、例えば __adddf3() など)。
目次: OpenCL
引き続き、独自アクセラレータのテンプレート実装pocl/lib/CL/devices/accelの細かな問題を調べます。初期化を突破するとカーネルのビルドclBuildProgram() でコケます。
POCL: in fn compile_and_link_program at line 664: | ERROR | CL_COMPILER_NOT_AVAILABLE Cannot build a program from sources with pocl that does not have online compiler support
原因はdev->compiler_availableがCL_TRUEになっていないことです。初期化が足りないようです。
// pocl/lib/CL/pocl_build.c
cl_int
compile_and_link_program(int compile_program,
int link_program,
cl_program program,
cl_uint num_devices,
const cl_device_id *device_list,
const char *options,
cl_uint num_input_headers,
const cl_program *input_headers,
const char **header_include_names,
cl_uint num_input_programs,
const cl_program *input_programs,
void (CL_CALLBACK *pfn_notify) (cl_program program,
void *user_data),
void *user_data)
{
...
/* clCreateProgramWithBuiltinKernels */
/* No build step supported at the moment for built-in kernels. */
if (program->builtin_kernel_names)
continue;
/* clCreateProgramWithSource */
else if (program->source)
{
#ifdef OCS_AVAILABLE
if (device->compiler_available == CL_TRUE) //★このif文が成立せず
{
POCL_MSG_PRINT_INFO ("building from sources for device %d\n",
device_i);
error = pocl_llvm_build_program (
program, device_i, program->compiler_options,
program_bc_path, num_input_headers, input_headers,
header_include_names, (create_library ? 0 : link_program));
POCL_GOTO_ERROR_ON ((error != 0), build_error_code,
"pocl_llvm_build_program() failed\n");
}
else
#endif
{ //★こちらにきてエラーになってしまう
APPEND_TO_MAIN_BUILD_LOG (
"Cannot build a program from sources with pocl "
"that does not have online compiler support\n");
POCL_GOTO_ERROR_ON (1, CL_COMPILER_NOT_AVAILABLE, "%s",
program->main_build_log);
}
}
他の実装を眺めるとpocl_init_default_device_infos() を呼んで解決しているようなので、先達に習いpocl_accel_init() でpocl_init_default_device_infos() を呼び出すように書き換えましょう。
もともとあったdev->version, dev->available, dev->profileの初期化はpocl_init_default_device_infos() が行いますから、削除しても良いかもしれません。残っていても特に害はないと思いますけど。
現状では拡張機能dev->extensions = "cl_khr_fp64" を設定しないとdouble型を使ったときにビルドエラーになります。最終的に必要な拡張機能がはっきりするまでは必要になったものを順次追加する形で、次に進みましょう。
拡張機能はただ書けば動くわけではなくOpenCL Cコンパイラが対応している必要があります。当たり前ですね。poclが使っているコンパイラは大御所LLVMですから、RISC-V向けだけ機能が欠けていることはないでしょう。たぶん。
ビルド対象はRISC-Vにしたいので、
dev->llvm_target_triplet = "riscv32";
dev->llvm_cpu = "generic-rv32";
としました。llvm_target_tripletはclang -cc1の -tripleオプションに渡されます。またllvm_cpuは -target-cpuオプションに渡されます。有効な値を調べる方法は、
$ clang -print-targets Registered Targets: aarch64 - AArch64 (little endian) aarch64_32 - AArch64 (little endian ILP32) aarch64_be - AArch64 (big endian) amdgcn - AMD GCN GPUs ... riscv32 - 32-bit RISC-V riscv64 - 64-bit RISC-V sparc - Sparc sparcel - Sparc LE sparcv9 - Sparc V9 systemz - SystemZ ...
$ clang -target=riscv32 -print-supported-cpus Debian clang version 11.0.1-2 Target: riscv32 Thread model: posix InstalledDir: /usr/bin Available CPUs for this target: generic-rv32 generic-rv64 rocket-rv32 rocket-rv64 sifive-e31 sifive-u54 Use -mcpu or -mtune to specify the target's processor. For example, clang --target=aarch64-unknown-linux-gui -mcpu=cortex-a35
GCCはコマンド名がアーキテクチャ名そのものですし、LLVMはヘルプで対応アーキテクチャがわかります。どちらも親切で良いですね。
ここまでの変更を反映すると、
// pocl/lib/CL/device/accel/accel.cc
cl_int pocl_accel_init(unsigned j, cl_device_id dev, const char *parameters) {
AccelData *D = new AccelData;
dev->data = (void *)D;
pocl_init_default_device_infos (dev);
//SETUP_DEVICE_CL_VERSION(1, 2); //★pocl_init_default_device_infosが初期化するのでいらない
dev->type = CL_DEVICE_TYPE_CUSTOM;
dev->long_name = (char *)"memory mapped custom device";
dev->vendor = "pocl";
//dev->version = "1.2"; //★pocl_init_default_device_infosが初期化するのでいらない
//dev->available = CL_TRUE; //★pocl_init_default_device_infosが初期化するのでいらない
dev->extensions = "cl_khr_fp64"; //★cl_khr_fp64を追加
//dev->profile = "FULL_PROFILE"; //★pocl_init_default_device_infosが初期化するのでいらない
dev->max_mem_alloc_size = 100 * 1024 * 1024;
dev->llvm_target_triplet = "riscv32"; //★追加
dev->llvm_cpu = "generic-rv32"; //★追加
dev->final_linkage_flags = final_ld_flags;
if (!parameters) {
POCL_ABORT("accel: parameters were not given\n");
}
まだまだ変更が必要ですが、こだわるのは後にして次に進みます。
目次: LLVM
準備が終わりましたらClang/LLVMをプログラムから呼びましょう。
int main(int argc, char *argv[])
{
bool success;
clang::CompilerInstance CI;
clang::CompilerInvocation &build = CI.getInvocation();
// 引数の配列を作成する
std::vector<const char*> vec_args;
vec_args.push_back("-I/usr/include/c++/10");
vec_args.push_back("-I/usr/include/x86_64-linux-gnu/c++/10");
vec_args.push_back("-I/usr/include/c++/10/backward");
vec_args.push_back("-I/usr/lib/llvm-11/lib/clang/11.0.1/include");
vec_args.push_back("-I/usr/include/x86_64-linux-gnu");
vec_args.push_back("-I/usr/include");
vec_args.push_back("-I/path/to/llvm-project/_install/include");
// エラーメッセージを出力するために使われるクラス
llvm::IntrusiveRefCntPtr<clang::DiagnosticIDs> diagID = new clang::DiagnosticIDs();
llvm::IntrusiveRefCntPtr<clang::DiagnosticOptions> diagOpts = new clang::DiagnosticOptions();
clang::TextDiagnosticBuffer *diagBuffer = new clang::TextDiagnosticBuffer();
clang::DiagnosticsEngine diags(diagID, diagOpts, diagBuffer);
CI.createDiagnostics(diagBuffer, false);
// コンパイラ呼び出し用のインスタンスを作成する
llvm::ArrayRef<const char*> ref_args(vec_args.data(), vec_args.data() + vec_args.size());
success = clang::CompilerInvocation::CreateFromArgs(build, ref_args, diags);
// コンパイラフロントエンドのオプション設定
// 入力ソースコード: test.cpp
// 出力ソースコード: test.preproc.cpp
const char *source_file = "test.cpp";
const char *preproc_file = "test.preproc.cpp";
clang::FrontendOptions &fe = build.getFrontendOpts();
clang::InputKind ik = clang::InputKind(clang::Language::CXX);
clang::FrontendInputFile fif = clang::FrontendInputFile(source_file, ik);
fe.Inputs.clear();
fe.Inputs.push_back(fif);
fe.OutputFile.assign(preproc_file);
// プリプロセスのオプション設定
// 言語: C++11
clang::PreprocessorOptions &po = build.getPreprocessorOpts();
clang::LangOptions *la = build.getLangOpts();
llvm::Triple triple = llvm::Triple();
build.setLangDefaults(*la, ik, triple, po.Includes, clang::LangStandard::lang_cxx11);
// 下記のようにオプションの一部だけ変えることもできる
//la->CPlusPlus = true;
//la->CPlusPlus11 = true;
// プリプロセスのオプション
// コメント、定義済みマクロなどは出力しない
clang::PreprocessorOutputOptions &poo = build.getPreprocessorOutputOpts();
poo.ShowCPP = true;
poo.ShowComments = false;
poo.ShowLineMarkers = false;
poo.ShowMacros = false;
poo.ShowMacroComments = false;
poo.RewriteIncludes = false;
// プリプロセス実行(失敗したらエラーログを出力する)
clang::PrintPreprocessedAction Preprocess;
success = CI.ExecuteAction(Preprocess);
if (!success) {
get_build_log(diagBuffer, (CI.hasSourceManager()) ? &CI.getSourceManager() : nullptr);
}
}
残念ながらこの呼び出し方が正解とは断言できません。探した限りではどう呼び出すべきか書かれたドキュメントも見当たりませんでした。上記の例はpoclを参考にしており、大きな間違いはないはずですが……。何かやらかしていたら教えていただけると嬉しいです。
動作確認はLLVM 12で行いました。他のバージョンだとAPIの引数などが変わっているので、ビルドすら通らないと思います。LLVMの困ったところですね……。
上記のサンプルでは引数で -Iオプションを使ってインクルードパスを指定します。インクルードパスは頑張ってヘッダファイルがある場所を調べても良いですが、おそらく同じ名前のヘッダが複数の場所にあって混乱すると思いますから、PCで動作しているClang++ から拝借するのが簡単です。
$ clang++ test.cpp -v Debian clang version 11.0.1-2 Target: x86_64-pc-linux-gnu Thread model: posix InstalledDir: /usr/bin Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/10 ... #include "..." search starts here: #include <...> search starts here: /usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10 /usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/x86_64-linux-gnu/c++/10 /usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10/backward /usr/lib/llvm-11/lib/clang/11.0.1/include /usr/include/x86_64-linux-gnu /usr/include End of search list. ...
いろいろなメッセージが出力されますが、インクルードパスは "search starts here:" の辺りに書かれています。出力は特に捻りはなくディレクトリ名そのものですので、頭に -Iを足せばオプションの出来上がりです。
プリプロセスを実行します。テスト用のプログラムは下記のとおりです。
#include <iostream>
int main(int argc, char *argv[])
{
// This is comment
std::cout << "Hello, world!!" << std::endl;
}
$ make $ ./clang_test
ファイル名などは完全に決め打ちのため引数は必要ありません。実行に成功するとプリプロセス後のソースコードtest.preproc.cppが作成されているはずです。
namespace std
{
typedef long unsigned int size_t;
typedef long int ptrdiff_t;
typedef decltype(nullptr) nullptr_t;
}
...
static ios_base::Init __ioinit;
}
int main(int argc, char *argv[])
{
std::cout << "Hello, world!!" << std::endl;
}
私の環境で実行したところ27,000行くらいあるファイルになりました。たった1つしかヘッダをincludeしてないのに凄まじい行数に展開されます。コメントは消えていますが、オプションを変更すれば残すこともできます。PreprocessorOutputOptionsのShowComments = trueにすると残ります。
$ g++ test.preproc.cpp $ ./a.out Hello, world!!
プリプロセス後のソースコードをg++ などに渡すとコンパイル可能なので、おそらく変な出力にはなっていないでしょう。
目次: LLVM
LLVMやClangは実行する方法が2つあります。1つ目はみなさまお馴染みのコマンドラインから実行する方法で、2つ目はプログラムからClangのライブラリを通して実行する方法です。
特に後者のプログラムから実行する方法はGCCでは真似できませんから、LLVMならではの機能と言えるでしょう。ただ、ちょっとインタフェースが不安定というか、バージョンによってちょいちょい変わって動かなくなるようで、そこは玉に瑕ですね。
Clang/LLVMをプログラムから実行するにはいくつか準備が必要です。大まかに分けるとLLVMのビルド&インストールと、ヘッダおよびライブラリパスの指定です。
ビルドは以前もチャレンジしました(2019年3月26日の日記参照)。基本的にはcmakeとmake(またはninja)です。それは変わりませんが、いくつか追加したいオプションがあるので再掲します。
$ cmake \ -G Ninja \ ../llvm \ -DCMAKE_INSTALL_PREFIX=`pwd`/../_install \ -DCMAKE_C_COMPILER=clang \ -DCMAKE_CXX_COMPILER=clang++ \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DBUILD_SHARED_LIBS=ON \ -DLLVM_ENABLE_ASSERTIONS=ON \ -DLLVM_TARGETS_TO_BUILD="X86;RISCV;NVPTX" \ -DLLVM_USE_LINKER=lld \ -DLLVM_BUILD_LLVM_DYLIB=OFF \ -DLLVM_LINK_LLVM_DYLIB=OFF \ -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra;compiler-rt;debuginfo-tests;libc;libclc;libcxx;libcxxabi;libunwind;lld;lldb"
ざっくり意図を説明すると下記のとおりです。オプションの正確な意味についてはLLVM公式ドキュメント(Build LLVM with CMake - LLVM 12 documentation 参照)を見てください。
CMakeの実行が成功したら、ninja installを呼びましょう。インストールまで進むはずです。
ヘッダインクルードパスの指定、ライブラリパスの指定のためにMakefileを書きます。パスの細かい値について心配する必要はありません。llvm-configというツールが用意されており、ほぼ全て自動的に用意してくれます。Makefileの一例を示すと、
LLVM_CONFIG_PATH = /path/to/llvm-project/_install/bin
LLVM_CONFIG = $(LLVM_CONFIG_PATH)/llvm-config --link-shared
CPPFLAGS = $(shell $(LLVM_CONFIG) --cppflags)
CFLAGS = $(shell $(LLVM_CONFIG) --cflags) -g
CXXFLAGS = $(shell $(LLVM_CONFIG) --cxxflags) -g
LDFLAGS = $(shell $(LLVM_CONFIG) --ldflags)
LIBS = -lclang-cpp $(LLVM_CONFIG) --libs --system-libs engine)
clang_test: main.o
$(CXX) $(CXXFLAGS) $(LDFLAGS) -o $(APP) $< $(LIBS)
基本的にはllvm-config --xxxflagsとするとオプションに指定すべき文字列が出力されますから、素直に各種FLAGSに渡すだけです。もちろん何かオプションを追加するのも自由です。例では -gを足しています。
LIBSのところがちょっと格好悪いのは、llvm-configでlibclang-cppにリンクするような方法が見当たらなかったからです。良い方法をご存知の方は教えていただけると嬉しいです。
これで準備完了です。続きは次回に。
目次: ALSA
いつもわからなくなるのでメモしておきます。mplayerにてイコライザーを使う方法です。最近はmpvと呼ぶんですかね?
コマンドはmpvを使いますが、実はイコライザー機能はffmpegの一部であるlibavfilter.soに頼っています(avfilterのドキュメントへのリンク)。この構造は一見しただけではわかりにくく、ヘルプを探すときに非常に難儀しました。設定方法も独特でいつも書き方がわからなくなります。
イコライザーはsuperequalizerという名前です(superequalizerのドキュメントへのリンク)。18バンド指定できます。各バンドがどの周波数帯に対応するかはドキュメントを見てください。
$ mpv --no-video --af=volume=0.8,superequalizer=1.2:1.5:1.5:1.2:1.2:1:1:1:1:1:1:1:1:1:1:1:1:1 a.mp4 Video --vid=1 (*) (h264 480x360 6.000fps) (+) Audio --aid=1 (*) (aac 2ch 44100Hz) AO: [pulse] 44100Hz stereo 2ch float A: 00:00:01 / 00:04:40 (0%) Cache: 278s/9MB
上記の例では、映像を出さない(--no-video)、音割れ防止の為にvolumeで8割くらいに音を下げる、superequalizerの18バンドを全て設定しています。superequalizer=1b=1.2:2b=1.5のようにすると特定のバンドだけ設定変更できます。便利な方を使ってください。
$ mpv --version mpv 0.32.0 Copyright © 2000-2020 mpv/MPlayer/mplayer2 projects built on UNKNOWN ffmpeg library versions: libavutil 56.51.100 libavcodec 58.91.100 libavformat 58.45.100 libswscale 5.7.100 libavfilter 7.85.100 libswresample 3.7.100 ffmpeg version: 4.3.2-0+deb11u2
動作確認に使ったmpvのバージョンも記録しておきます。なぜならffmpegやmpvはたまにインタフェースが激変するので、将来的に同じ方法が通用しなくなる可能性が高いからです。使用しているディストリビューションはDebian Testingです、今はDebian 11相当みたいですね。
なぜかbuilt on UNKNOWNになっていて若干気になりますけど、特に害なさそうだから良いのかな……。
目次: マンガ紹介
書籍通販のhontoがこんなキャンペーンをやっています。
このキャンペーン画像を見たときの率直な感想としては、どんな人間を想定したら、読書一生分がたった93万円に収まるのか?でした。マンガしか読んでない自分でさえ100万じゃ10年も持ちません。
思い込みで文句を言うのは良くないなと思って、統計データを見ました。総務省統計局 - 読書に関する支出(2018年)によると、1世帯、読書の支出が年間10,628円(電子書籍含まず)です。電子書籍を含む値段で考えたとしても、さほど変わりません。電子書籍を最も購入している30代(世帯主の年齢)でも1,736円で、読書支出は12,000円程度だからです。
世帯の読書支出10,628円x日本人の平均寿命84年 = 892,752円となり、hontoのキャンペーン金額と大体同じくらいになります。あながち間違った数値でもなかった、ということですね。
先程のデータを見ていて何が驚いたって、1世帯で1年間たった1万円しか本を買わないことです。この時点で少ないなと思うんですけど……。1世帯には複数人が生活していますので、1人あたりの支出も計算してみます。
世帯の平均人数はe-Statで調べることができます。平均世帯人員、年次別(平成27年国民生活基礎調査 世帯票 報告書掲載 年次推移 表番号7)を見ると、2015年で1世帯平均2.49人です。
世帯あたり読書の支出は1年10,628円(書籍7,478円、雑誌3,150円)割ることの、日本の平均世帯人数2.49人(減少傾向)ですから、1人あたり1年で4,268円(書籍3,003円、雑誌1,265円)です。さらに少なくなりました。
例えば、週刊少年ジャンプ(定価270円x 50冊 = 13,500円)をもれなく買うだけで3倍以上の支出になります。普段全く本は買わない、くらいじゃないと1年4,268円は厳しいです。世間の生活が想像できません……。
< | 2021 | > | ||||
<< | < | 07 | > | >> | ||
日 | 月 | 火 | 水 | 木 | 金 | 土 |
- | - | - | - | 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 |
合計:
本日: