【c/c++】OpenMPの基礎的な使い方+並列処理で意識すべきこと【OpenMP】

OpenMPの基礎的な使い方を通して並列処理で意識すべきことを書きます。
ここではコンパイル方法や詳細な内容については触れませんが、OpenMPのより詳細な使い方については参考文献を参照してください。
記事の内容をざっくりとまとめると、並列化の上で知っておかなければならないことは大まかに以下の3点です。

  • その処理は並列化可能か
  • 負荷の分散は十分考えられているか
  • 共有変数への不正なアクセスは発生しないか


並列化の基本

並列化の可否

そもそもコードを並列化するためには、行われる処理がそれぞれ独立している必要が有ります。
例えば以下のようなコードではa = 1;b = a + 1;は並列化することはできません。a = 1;の結果を得なければb = a + 1;を計算できないからです。

int a, b;

a = 1;
b = a + 1;

並列化可能なコードとは、並列実行したいそれぞれの処理が別の処理に対して影響を与えないようなコードです。

セクションごとの並列化

ここでは、並列化するコードの単位をセクションと呼びます。
処理Aをやっている間に並行して処理Bを行う(ex.サーバーからファイルを落とす間に別の処理を行うなど)ということができれば、場合によっては大きな高速化が期待できます。

OpenMPを用いたセクションでの並列化

以下はOpenMPを用いた2つのセクションでの並列化の例です。(OpenMP - PukiWiki for PBCG Lab より引用)

#pragma omp parallel sections num_threads(2)
{
    #pragma omp section
    {
        /* セクション1 */
    }
    #pragma omp section
    {
        /* セクション2 */
    }
}
セクションごとの処理コストの均等化の必要性

処理の並列化は、上手くやればコア数分(ハイパースレッディングなどが有ればそれ以上)の高速化が期待できます。一方、セクションに分割して並列化を行ったとしても、セクションごとの処理コストの均等化を行わなければ、最終的には一番コストの重いコードの実行時間までしか高速化できない点には注意が必要でしょう。
例として処理A, B, C, D(処理コスト:A > B > C > D)が有ったとき、セクション1が処理A, B、セクション2がC, Dとして並列化を行ってしまうと、全体ではセクション1の実行時間が足を引っ張るため思ったほど高速化しない、ということになります。これは後述するループの並列化においても同様で、例えばループ中の処理コストがどこかに偏っている場合、分割手法によってはあまり高速化できません。
このように、並列化によって高速化したい場合、それぞれのセクションの処理コストをできるだけ均一にすることが重要となります。

ループの並列化

特に並列化で触れる機会が多いのは、ループの並列化でしょう。
ループの並列化は『一番外側の(iに関する)ループをプロセス/スレッドに分割して並列化し、一番内側のループ(kに関するループ)をSIMD命令で並列化する*1』というやり方が基本です。
「分割すればした分だけ早くなるのでは?」と感じるかもしれませんが、プロセス/スレッドの作成やプロセス間通信などにもコストが掛かることから、コア数以上の並列化を行っても実行速度は低下する場合が多いです。一般的な環境では一番外側のループを並列化した時点でコア数を使い切ってしまうため、分割は一番外側だけにするというのがベターとしました。
特殊な例として、スーパーコンピュータのようにコア数が物凄く多い環境では、コア数を使い切るための分割の最適化が必要となります。要はコア数とスレッド分割数をできるだけ近づけた方がいいということです。

OpenMPを使ったループの並列化

OpenMPでは、特に設定しなければ利用する論理コア数に合わせて自動でスレッドの分割数を決定してくれます。
単純に書くと以下のようになります。OpenMPでは更に自由度の高い(=複雑な)並列化の指定も可能ですが、ここでは省略します。

#pragma omp parallel for
for(int i = 0; i < ... ; i++){
    for(int j = 0; j < ... ; j++){
        for(int k = 0; k < ... ; k++){
            /* 何らかの処理 */
        }
    }
}
schedule指示句によるループスケジューリング

前述のとおり、ループ中に処理コストの偏りが有ればループを均等に分割しても十分に高速化しないことが考えられます。このようなループはschedule指示句を追加することで簡単に高速化できる場合が有ります。
schedule指示句はparallel for構文の後ろに付けることで、ループを大まかにどのように分割するかを指定できます。
勉強不足で挙動について理解できていないので詳しく書けませんが、自分で確認したところ、計算量がインデックスiの2乗に比例するようなプログラムでschedule(guided)を指定したところ、20%の高速化が確認できました。
以下のページが参考になると思います。
OpenMP* ループ・スケジュール | iSUS

共有メモリへのアクセス

並列処理では複数のプロセスやスレッドが同時に処理を行うため、スレッド間で共有される変数が予期せぬタイミングで書き換えられ、結果が不正となる場合があります。
共有メモリへのアクセスは並列化の上で意識すべき最も重要な内容であるため、1章を割いて説明します。

共有メモリの不正な読み書き

以下のコードを実行すると、本来であれば5050が出力されますが、実際はそうならない場合があります。

omp_set_num_threads(100); //変数の不正な上書きを誘発するために実行スレッド数を多めに設定

int count = 0, factor;
int i;

#pragma omp parallel for
for(i = 1; i <= 100; i++) {
    factor = i -1;
    count += factor;
    count++;
}

printf("%d", count);
なぜこうなったのか

CPUは以下のような手順で命令を実行します。

  1. 変数をメモリから読み出す
  2. 処理を行う
  3. メモリへ結果を書き戻す

一方並列化して実行を行うと、タイミングによっては、以下のように共有メモリ上の演算結果が破壊されてしまいます。

  1. 変数をスレッド1が読み出す
  2. 変数をスレッド2が読み出す ← スレッド1による処理の結果は反映されていない!
  3. スレッド1, 2が処理を行う
  4. スレッド1がメモリへ書き戻す
  5. スレッド2がメモリへ書き戻す ← スレッド1による処理の結果が上書きされる!

つまり、上の例では変数factorが複数スレッドから同時に書き換えられたり、countが同時に複数のスレッドから読み出されるといった事態が発生すると結果が不正になるということです。

不正なアクセスを防ぐためには

このような不正なアクセスは、複数のスレッドから呼び出される変数に対して大まかに3通つの手段を講じることで防げます。

  1. スレッドごとにローカル(プライベート)な変数にする
  2. 変数へのアクセスをロックする
  3. 個別の書き込みスペースを用意する
スレッドごとにローカル(プライベート)な変数にする

例では、変数factorは毎回ループの先頭で初期化されています。このような変数は各スレッドごとにローカルな変数とすることで、別スレッドからの操作が起こらないようにできます。
具体的な方法として、c/c++のバージョンが高ければ変数factorをループの先頭で宣言するように変更するのが最も単純な方法です。
OpenMPの機能としては、以下のようにprivate([変数名])指示句を追加することで、指定した変数をプライベートなものとして扱うことができます。

#pragma omp parallel for private(factor) 
for(i = 1; i <= 100; i++) {
    factor = i -1;
    count += factor;
    count++;
}
変数へのアクセスをロックする

例では、変数countへのアクセスをロックすることで不正な読み書きを防ぐことができます。OpenMPではatomic指示句やcritical指示句を用いる、ロックを行うといった手法が有ります*2
実行速度は『atomic指示句 > critical指示句 > ロック』の順で高速です。ただしatomic指示句は利用可能な命令に制限があるため、注意が必要です。
以下はcritical指示句を利用した例です。この例ではatomic指示句が使えなかったので、critical指示句を利用しています。

#pragma omp parallel for private(factor)
for(i = 1; i <= 100; i++) {
    factor = i -1;
    #pragma omp critical
    {
        count += factor;
        count++;
    }
}
個別の書き込みスペースを用意する

例では一々変数countへの書き込みを行っていることが原因で不正なアクセスが発生しています。
先ほどは変数へのアクセスをロックすることでこの問題を回避していましたが、答えを書き込むための配列int temp[100]を用意しておき、あるiに対する計算結果をtemp[ i-1 ]に格納し、最後に総和を計算することによっても回避することができると考えられます。
OpenMPではreduction指示句を用いることで、自動的に『個別の書き込みスペースの確保→集計』までを行ってくれます。
以下の例では、critical指示句の代わりにreduction指示句に総和を指定することでこの問題を回避しています。

#pragma omp parallel for private(factor) reduction(+:count)
for(i = 1; i <= 100; i++) {
    factor = i -1;
    count += factor;
    count++;
}

reduction指示句に指定可能な演算子の一覧はこちらをご覧ください。

オマケ1(OpenMPを利用する際のCMake)

cmake_minimum_required(VERSION 3.10)
project([プロジェクト名] C)

find_package( OpenMP )

set(CMAKE_C_STANDARD 11)
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}")

add_executable([プロジェクト名] [ソースコード].c)

オマケ2(SIMD命令に関する記事を作成しました)

wrongwrong163377.hatenablog.com

*1:この記事ではSIMD命令には触れません。

*2:これらは厳密には処理の内容が異なりますが、得られる結果は同じなのでまとめて取り扱います。

【Java】Functionを使ってみる

タイトル通り、Java 8で追加されたFunctionが便利だったので書きます。

関数型インターフェース関連でとても参考になるページ

Java関数型インターフェースメモ(Hishidama's Java8 Functional Interface Memo)

Functionとは

Function<T,R> は引数Tを受け取って戻り値Rを返す関数を変数っぽく扱えるものです。大体cの関数ポインタとか.Netのdelegateみたいなもんです。
例えば以下の例では、funcintを受け取って処理を行い、intを返します。

Function<Integer, Integer> func = input -> input * 3; //int inputに3を掛けたものが帰る
func.apply(3); //呼び出し例、3 * 3 = 9が帰る

正直うまく書けて無いと思うので、以下の記事を参照してください。。。
www.atmarkit.co.jp

なにがうれしい?

同じxを入力とする一次関数と二次関数があり、それぞれパラメータはある程度一定であるような場合を例に書きます。結論のソースコード全体は一番下に置きます。

素直に書くと

素直に書くと以下のようになります。
変化するのはxだけなのに何度も何度も変数を書いて関数を呼び出して……というのは一々書くのがダサくて面倒くさいです。各関数のパラメータも、何らかのラベル付けをした方がTypoによる事故も起きにくいと思われます。

private static int linearFunction(int a, int b, int x){
    return a * x + b;
}
private static int quadricFunction(int a, int p, int q, int x){
    return a * (x - p) + q;
}
public static void main(String[] args) {
    int x = 2;
    System.out.println(linearFunction(1, 1, x));
    System.out.println(quadricFunction(1, 1, 1, x));
}
Function + EnumMap

これをFunction + EnumMapで書き直すと以下のようになります。
Enumでラベルを付けてマップに登録して呼び出してます。呼び出しがスッキリしたのとかはうれしいです。もうちょっといいやり方が有る気もしますがとりあえず気にしないことにします。

private enum e{
    La1b2,
    Qa1p1q1,
}

private static int linearFunction(int a, int b, int x){
    return a * x + b;
}
private static int quadricFunction(int a, int p, int q, int x){
    return a * (x - p) + q;
}

private static final EnumMap<e, Function<Integer, Integer>> map;
static {
    map = new EnumMap<>(e.class);
    map.put(e.La1b2, x -> linearFunction(1, 2, x));
    map.put(e.Qa1p1q1, x -> quadricFunction(1, 1, 1, x));
}

public static void main(String[] args) {
    System.out.println(map.get(e.La1b2).apply(2));
    System.out.println(map.get(e.Qa1p1q1).apply(2));
}
続きを読む

【日記】はじめての外部発表を終えて

詳細は書きませんが、卒業研究について外部発表をしてきました。今年度は1回以上の外部発表を目標としていましたが、早くも達成できた感じです。

できたこと

発表と発表準備を合わせて、今までの研究での疑問点や改善点が整理されたのは非常に良かったです。これによって今後の研究において取り組むべき内容に関して整理することができました。

発表に関しては、自分の研究内容について「これがあれば助かる!」という旨の感想を直接頂けたことが非常に嬉しかったです。発表への反応を通じて研究の重要性を再認識することができました。

できなかったことと反省

折角自分の作ったものに多大な興味を抱いて頂けたにもかかわらず、発表の完成度が低くなってしまったことが最大の心残りです。とにかく講義とレポートで資料作りと発表練習に時間を取ることができませんでした。

特に大きな反省点は、今回の発表は20+10分でしたが、これに合わせた発表資料作りができなかった点です。これまでの『削る』発表のノリで作業した時間が長すぎ、発表練習でのセリフも忘れ、最終的には発表時間を大きく残して終わってしまいました。

また先行研究についての理解・読解も不足しており、検証済みの内容に関する質問にも「分からない」と返答してしまいました。次はこういったことが無いようにしたいです。

今後に向けて

何にせよ、初めての外部発表は終わってしまいました。今後は整理した研究計画に基づき研究を進めつつ、並行して関連論文の読み込みを進めていこうと思います。研究に関しては現状明確な課題が3点有るので、修士課程を通してこの解決に取り組みます。

勿論結果を出した上での話ですが、次は自分が納得できるだけの発表ができるようにがんばります。少なくとも今年度末には発表に繋げたいですね。

インターンに行きたいこともあって各地のイベントに参加するなどしており、研究に掛けられる時間が十分ではありませんが、できるだけ効率的に進めていくことを目標とします。

【自作PC】COMPUTEX TAIPEI 2018で紹介されたAORUSブランドのDDR4メモリーについて【日記】

GIGABYTEがAORUSブランドでDDR4メモリーを発売するようです。

www.gdm.or.jp

まだ名前も出ていませんが、詳細が出たらまとめ記事に追加しようと思います。

製品の特長

RGB LEDもそうですが、ダミーモジュールが付いてきます。

 メモリスロットが4スロットでも、価格などの都合で2スロットしか埋められずに見栄えが悪くなることは多いので、これはいいかもしれないと思いました。

ヒートシンク

TwitterでAORUSの中の人に測定していただくことができました。

一応スクショを。

f:id:wrongwrongwrongwrong163377:20180606031658p:plain

実測40mmだそうです。RGB LED搭載製品はヒートシンク高が大きい製品が多いため、これも大きな特徴かもしれませんね。

少し残念な点

(ここからは愚痴がメインです)

RGB Fusion以外での制御は想定していないそうです。

マザーボードに比べDDR4メモリーはそうそう買い換えるものではないため、これを変えばGigabyte製品から離れにくくなるというわけですね。

Gigabyteに限った話ではなく、RGB LEDによるライティング界隈では「自社ユーザーを離さないために独自規格を継続しつつ自社ブランド製品を充実させる」戦略が取られており、ユーザーとしては頭が痛いです。NZXTやらCorsairやら周辺機器メーカーもやっているわけで、特にNZXTはマザーボードまで投入してきているわけですが、何せ現状でも規格が乱立している訳で、ここから更に増やすのかと……。

製品そのものはどれも魅力的ですが、とにかく「自分で選んで組み合わせる」のが自作の大きな魅力でもあるわけで、ブランド縛りはあくまで「他社製品も選べるけどこの組み合わせがカッコいい」という程度の緩いものであることが健全なんじゃないかなあと思います。

ということで、規格の統一を切に願います。

その他

そう言えば先日@gigabyte_newsにブロックされていることに気付き、@AORUS_JPさんにメンションを飛ばした所「話し合ってみる」との返信を頂きましたが、今確認した所まだブロックされたままでした。

ブロックされた原因として思い当たる件があんまりにもアレなので早くブロック解除して欲しいというか、ブロックの原因があの件なら@gigabyte_newsの担当さんそのままで大丈夫なんですかねって感情があります。(あ、@AORUS_JPさんにはお世話になりっぱなしで好感情しか無いです)

プリンターで一部日本語が印刷されず出力がおかしくなる時に試してみること

pdfをUSBメモリから印刷すると、途中の日本語が表示されずに画像のような状態で印刷される場合があります。

f:id:wrongwrongwrongwrong163377:20180531160110j:plain

対処法

pdfを圧縮すると問題なく印刷できる場合があります。圧縮はpdf圧縮を行ってくれるwebサービスが幾つかあるので、そちらを利用してみて下さい。

注意点

サービスの設定によっては画像の品質なども劣化します。後述しますが、消えて欲しくないフォント設定まで消えることが考えられるので、圧縮後は一度目を通しておくべきでしょう。

原理

特定できている訳ではなく推測ですが、上手く印刷できない原因はプリンタが印刷できないフォントがpdfに混じっていることだと考えられます。pdf圧縮はフォントの情報を抜くなどの処理が行われるため、印刷できない情報が抜けて印刷できるようになるのだと思われます。

【c/c++】Suffix ArrayとBWTに関するプログラムを作ってみた【c++17】

2018/6/25:concurrency::concurrent_vectorを使うべきだったがstd::vectorを使用しており、長い文字列の処理が不安定であったため、これを踏まえて全体に書き直し。



最近c++を触る機会ができたり、c++17の機能を使ってみたかったりしたので、課題にかこつけて取り組んでみました。

何を作ったか

Suffix Arrayと、Suffix ArrayからBWTの作成、BWTから元の文字列の復元をするプログラムを作りました。

Burrows-Wheeler Transform(BWT)とは

d.hatena.ne.jp

プログラム

ソースコード

プロジェクト全体はGitHubに上げています。
github.com

#include <algorithm> //sort用
#include <execution> //algorithmの並列実行ポリシー
#include <ppl.h> //parallel_for用
#include <concurrent_vector.h> //並列

#include <iostream>
#include <string>
#include <tuple>

//単語を入れたらSuffixのベクターを返す
concurrency::concurrent_vector<std::tuple<int, std::string>>
MakeSuffix(const std::string &str
){
    using namespace std;

    concurrency::concurrent_vector<std::tuple<int, std::string>> v;
    //parallel_forで並列処理
    concurrency::parallel_for(size_t(0), str.length(), [&str, &v](size_t i){
        v.push_back(make_tuple(i, str.substr(i, str.length())));
    });

    return v;
}

//Suffixをソート
void
DictionaryOrderSort(concurrency::concurrent_vector<std::tuple<int, std::string>> &Suffix
){
    using namespace std;
    sort(execution::par_unseq,
         Suffix.begin(), Suffix.end(),
         [](tuple<int, string> &t1, tuple<int, string> &t2) { return (get<1>(t1) <= get<1>(t2)); }
        );
}

//BWT系列を作成
std::string
MakeBWT(const std::string OriginalString,
        const concurrency::concurrent_vector<std::tuple<int, std::string>> &SuffixArray
){
    using namespace std;

    string s(OriginalString.length(), '0'); //容量固定のstringを配列っぽく使用
    concurrency::parallel_for(size_t(0), OriginalString.length(), [&](size_t i){
        if(get<0>(SuffixArray[i]) == 0) s[i] = OriginalString[OriginalString.length()-1];
        else s[i] = OriginalString[get<0>(SuffixArray[i])-1];
    });

    return s;
}

std::string
ReconstructionFromBWT(const std::string &BWT,
                      const unsigned int limit = INT_MAX // 何文字目まで再生するかの指定
){
    using namespace std;
    //タグ付け
    vector<tuple<char, int>> v;
    string temp = BWT;
    size_t val; //BTWの行末をまず入れる
    for(size_t i = 0; i < temp.length(); i++){
        v.emplace_back(temp[i], i); //emplace_backの中に書いた要素でデフォルトコンストラクタが呼ばれてるそうな
        if(temp[i] == '$') val = i;
    }
    //安定ソートで並べ替え、法則を得る
    stable_sort(execution::par_unseq, v.begin(), v.end());

    //リミットまで復元
    const int lim = max((int)temp.length()-(int)limit-1, 0);
    int j;
    for(int i = (int)temp.length() -1; i >= lim; i--){
        temp[i] = BWT[val];
        j = 0;
        while(val != get<1>(v[j])) j++;
        val = j;
    }

    return temp.substr(max(lim, 0), temp.length());
}

int main() {
    using namespace std;

    //string str = "abca$";
    string str = "internationalization$"; //入力
    cout << "Suffix\n";
    for(auto t : v){
        cout << get<0>(t) << "\t:" << get<1>(t) << '\n';
    }

    DictionaryOrderSort(v); //ソート
    cout << "\nSuffix Array\n";
    for(auto t : v){
        cout << get<0>(t) << "\t:" << get<1>(t) << '\n';
    }

    string BWT = MakeBWT(str, v); //BWT取得
    cout << "\nGet BWT" << '\n';
    cout << BWT << '\n';

    cout << "\nDecode" << '\n';
    if(str == ReconstructionFromBWT(BWT)) cout << "success!";
    else cout << "failed";


    cout << flush;

    return 0;
}

出力

「internationalization」という単語についての出力です。

解説

重要なのは大きく以下の2点です。

  1. Suffix ArrayからBWT系列を求める場合の行末の扱い
  2. BWT系列から文字列を復元する方法
Suffix ArrayからBWT系列を求める場合の行末の扱い

Suffix ArrayからBWTを求める場合、行末を区別しなければソート順が乱れるパターンが有るため、何らかの記号を補っておく必要があります。単語の末尾に「$」を加えているのはそのためです。

BWT系列から文字列を復元する方

原理の説明は飛ばします。
BWT系列からの復元は、BWT系列の各文字に数字でタグを付け、

vector<tuple<char, int>> v;
string temp = get<1>(BWT);
for(size_t i = 0; i < temp.length(); i++){
    v.emplace_back(make_tuple(temp[i], i));
}

BWT文字列を辞書順に安定ソートし、

stable_sort(execution::par_unseq, v.begin(), v.end());

BWTキーから順に『キーと同値のタグを見つける→文字列の先頭にBWT文字列[タグ]を加える→見つけた場所の番号のタグが入っている場所をキーとする』という操作を繰り返していくことで、

int val = get<0>(BWT);
int j;
for(int i = (int)temp.length() -1; i >= lim; i--){
    temp[i] = get<1>(BWT)[val];
    j = 0;
    while(val != get<1>(v[j])) j++;
    val = j;
}

行うことができます。ただし復元は末尾から行われます。
今回のプログラムではMakeBWT関数で行末の位置を保持して返すようにしていますが、行末記号は文字列中に有るため、保持が無くても復元は行なえます。

工夫した点

parallel_forを使う

ppl.hparallel_forを使って並列処理でSuffixを作りました。c++ラムダ式を触ってみたかったのですが、これで理解できたかなと思います。
場合によってはalgolithmfor_eachExecutionPolicyを指定する形とした方がppl.hに依存しなくなるので良いかなと思いました。……本音を言うとparallel_forの形が規格として取り込まれてくれるといいなと思います。
最初はparallel_forを使う時にconcurrent_vectorを使っていなかったせいで不安定になっていたのが反省点です。
参考資料:
parallel_forの書き方:
How to: Write a parallel_for Loop | Microsoft Docs
ラムダ式の書き方:
cpprefjp.github.io

std::sortの並列実行

c++17でalgorithmに並列実行オプションが追加されました。execution::par_unseqがそれです。c++17の機能、これしか使えませんでした……。
stable_sortexecution::par_unseqを指定するとコンパイルできなくて悩んでましたが、今朝アップデートをかけたら普通に動きました。先にアップデート確認しとけばよかったです。
参考資料:
c++17の並列アルゴリズムライブラリ関連:
faithandbrave.hateblo.jp

tupleの独自ソート

参考資料:
minus9d.hatenablog.com

感想

c++17の新機能ってことで畳み込み式とか使ってみたかったんですが、使う場面が思いつかず。c++17の新機能は強力に見えますが、実際に使う場面が思い浮かばない辺りが何とも……研究室の他の方のプログラムに導入して改善できそうならやってみたいですね。
並列処理に関してはthreadだのOpenMPだの試してみましたが、algorithm系を除けば結局parallel_forが使いやすいかなと感じました。プラットフォームに依存しないという意味ではOpenMPの方がいいかもしれませんが……。
後Clionの構文チェックが結構いい加減というか、tupleへのgetなんかにエラー表示が出て色々とよろしくない感じがあったのがダルかったです。
そもそもc++の書き方を大半忘れていて全然書けなかったのが辛い……。
もっと良いアルゴリズムも有るようですが、力尽きたのでここまで。

【Google ドキュメント】Google ドキュメントで使える等幅フォント

(※6/13 記事を全面的に差し替え)

ドキュメントにプログラムを貼り付ける時に見やすくなるように、Googleドキュメント/スプレッドシート/スライドで使える等幅フォントを探しました。

フォントの追加手順についても書きます。

フォントの選択/追加

以下の順番でやると分かりやすいと思います。

  1. 「フォントの追加」から等幅フォントを探す
  2. 使える等幅フォントの中から良さ気なものを探す
手順

フォント選択画面から「その他のフォント」を選びます。

f:id:wrongwrongwrongwrong163377:20180613150558p:plain

以下のような画面が出るので、「表示」のプルダウンから「等幅」を選びます。等幅以外にも色々なフォントを見ることが出来ます。

f:id:wrongwrongwrongwrong163377:20180613150713p:plain

フォントの使用例

Source Code Pro、Ubuntu Mono、Roboto Monoの3種類のフォントを試してみました。

f:id:wrongwrongwrongwrong163377:20180613152350p:plain

個人的にはUbuntu Monoが良さ気かな?と思ってます。

不満な点

日本語のフォントがもっと欲しいです。貼ったサンプルの通り、日本語は全く等幅では有りません。コメントが綺麗に表示されないのは困るので、もう少しなんとかしてほしいなと思います。