2021年12月30日木曜日

日本語訳:Apex Legends のALGSマッチを機械学習で解析した件 Using Machine Learning, I analyzed 2.3TB of competitive apex to track everything

This is a translation of Reddit  post Using Machine Learning, I analyzed 2.3TB of competitive apex to track everything into Japanese.

Apex Legends のゲームプレイについて機械学習を用いて分析した結果が Reddit に投稿されており話題になっていました。興味深い内容なので、翻訳サービスなどを用いて読んでいる日本の方も多いと思います。しかし、機械翻訳では判りづらい点もありますので、適当に日本語に翻訳したものを置いておきます。

英語にはあまり自信がなく、誤訳などもあるかもしれませんが、お気づきの点がありましたら @hasegaw までお知らせいただけますと幸いです。

予備知識

この投稿者は統計について研究されている大学院生で、 Apex Legends の ALGS 3大会のビデオから得られた情報をコンピュータに入力したり、もしくは関連するゲーム配信をコンピュータビジョンのアプローチから分析し、下記の内容の論文を書きました。

今回、コンピュータを用いてたくさんのデータを集計し、そこから規則性などを見いだす方法として機械学習(Machine Learning)と呼ばれるものが使われました。これは、最近AI(人工知能)として話題になっている技術のひとつです。

~~~

私は、以下の7つ問いについて分析した。

  • ジブラルタルは再現性が高く、そしてゲームを壊すほどに強力なのか?
  • コントローラ(Pad)プレイヤーは、本当に短距離の交戦を支配しているのか?エイムアシストは効きすぎているのか?
  • リコイルチートを使用するプロプレイヤーは存在するのか?
  • 最適な交戦時間は?
  • 最適な安置移動(Rotation)ははどれぐらいの速度か?
  • 最適なバックパックの構成は?
  • イロ(ELO)レーティングが最も高いプレイヤーは誰か。また、その理由は?


■概要

Kirk Matrix は、サッカーのランキングアルゴリズム Colley Matrix を基にカスタマイズしたものである。また、グループ競技における個人のレーティングのために、 Bradley-Terry ニューラルネットワークモデルを用いた。この方法は、シンプルな統計的原理と変数に基づき、柔軟で、バイアスがないようにしたものである。


わかったこと

ジブラルタル

ジブラルタルは時間あたりのダメージ量が(他のレジェンドより)低く、個人としての撃ち合いによる勝率が低く、受けるダメージ量も大きく、またキルタイム(TTK)が最も短い(訳者注:相手を落とす時間が短いという意味だと思います)。

ジブラルタルは、独立した(他部隊の介入がない)3v3 の交戦において、もっとも死亡しやすい。これは、次点のブラッドハウンドよりも 23.1% 高い確率であった。ジブラルタルは最もフォーカスされやすいキャラクタである。

ドームファイトによる交戦勝率は 50%-50% で、他のチームより特に優れているチームは存在しなかった。ドームファイトは平均48秒で、6人あたり4人がノックされる。

(訳者注釈)ここで言っているのは、接敵でジブラルタルが戦闘でフォーカスされやすく、落ちやすく、1プレイヤーとしての成績が低くなりやすいと統計的データに基づいて説明しているのであって、決して「チームとしての勝率が低い」という話ではありません。

他レジェンドとの入れ替わりもあるが、ジブラルタルのキャラクターとしてのELOは最下位であり、実証データもこれを裏付けている。


エイムアシスト

ALGSにおいては長距離交戦が非常に少なく、中距離交戦とまとめて扱う必要があった。

  1. ドームファイトの交戦勝率: KB/Mouse 54% : 46% Pad
  2. 短距離の交戦勝率: KB/Mouse 52% : 48% Pad
  3. 中・長距離の交戦勝率: KB/Mouse 51% : 49% Pad
  4. ワンクリップシーンでの勝率  KB/Mouse 38% : 62% Pad
  5. KB/Mouse の ADS 振り向き速度の優位性により、混沌としたドームファイトを制することを可能としている。
  6. エイムアシストがあったとしても、 Pad はあらゆる距離での接敵において KB/Mouse に対して劣っているが、一方でコントローラプレイヤーは即時キル(one clip)を繰り返すこともでき、良い結果にも悪い結果にもなる。
(訳者注:KB/Mouse のほうがトータルとしては優れているが、 Pad プレイヤーは下振れもすれば上振れすることも多いということになります)

チーター

驚いたことに、NA/EU/APAC NorthのALGSプレイヤーにおいてリコイルチートを使用するプレイヤーはひとりも検出されなかった。私はこの結果が正しいものと信じている。


最適な交戦時間

交戦時間よりも、サードパーティー(漁夫)がノックアウトを取るうえで大きな相関があると分かった。これは、間違いなく、キルフィード(キルログ)管理、サードパーティー(漁夫)されないこと、敵対チームの把握によるものである。なお、このセクションにおける知見は重大な欠陥があるため、断りなく使用するべきではない。

(※訳者注:要するに、うまく漁夫して、漁夫られないよう気をつけることが大事だとのこと)


最適な安置移動(Rotations)速度

ここでも、私は明確な答えを見つけることができなかったが、興味深い発見があった。 KB/Mouse プレイヤーの移動速度は、 Pad プレイヤーの移動速度よりも平均 6% 速いのである。この違いは、ランドマークからランドマークまで(??)の移動で、壁キック2回、スライドジャンプ4回に相当するものである。 Pad プレイヤーは、これらの動きを追加することにより、(※訳者注:何に対して?)移動を約 16 秒短縮できるかもしれない。


最適なバックパックの構成

残念ながら、こちらも明確な答えを見つけることができなかった。ただし、金バックパックをを持っていることが、Apex Legendsのどのアイテムよりも、勝利と強い相関があった。


上位ELOレーティング

(※訳者注:ELOレーティングとは、平均的な実力のプレイヤーと接敵した場合に予想された勝率に推定したものとされる。Wikipediaによれば対数が使われるとのこと。Redditの投稿にはELOのどこのように算出したかは記述がないが、投稿の最後にあるリンクを読み解けば細かいことがわかるかもしれない)

  1. Ras .9987

  2. Senoxe .9363

  3. Lou .9246

  4. Dezignful .9244

  5. Mo-Mon .8331

  6. Sweet .8075

  7. Genburten .8009

  8. Hardecki .7977

  9. Dooplex .7865

  10. Ojrein .78647

ImperialHalはわずか .00002 の差で次点。

  1. Hal .78645

利用したモデルはRasによって破綻されられるほどで、RasはELO算出に用いられた12の変数のうち11つが並め〜低いのだが、接敵時の移動速度については例外だ。接近戦やドームファイトにおいて、他の ALGS プレイヤーと比較して、Ras はわずか3分の1のダメージしか受けない。

現在のところ、Rasと他の競技者との差は、おもに彼の移動テクニック(※キャラコン)によるものである。他のプレイヤーが同等のテクニックを極めた場合、この差は大きく縮まるかもしれない。


私の経歴について:統計学の学士号、修士号を所得。本内容の論文を提出しており、通過し次第(2~3日)、本分析のグラフやデータを公開できるようになる見込みです。

使用したツールについて:すべてのツールはパブリックに利用可能なものですが、フォークされ、私の特定のニーズに合わせてカスタマイズされています。

〜〜〜

「所詮AIの判断だろ」といった意見もありますが、これは機械学習アルゴリズムを用いてALGSの成績を定量的に分析した結果なので、ALGSという限られたマッチ本数での分析によりバイアスはかかっているのでしょうが、ここに書かれた分析の範囲では、だいたい正しいことだろうなと思っています。

私は以前 IkaLog というスプラトゥーンのビデオから自動的に戦績を解析・出力するソフトを作成し、これを stat.ink というサイトがとりまとめ、蓄積されたデータを基に分析する方もいらっしゃったのを思い出しました。ゲームとはいえ、データを分析して考察するのはとても面白いですね。 Apex Legends に限らず、色々なゲームで、一般プレイヤーのプレイも含めて、このような分析ができると、いろいろと面白いのでしょうが。


おまけ:過去に投稿して話題になったツイートのスレッド。ネットゲームのチート対策は攻撃者のほうが有利で、対策は色々と難しいところもあるということを感じてもらえればと思います(もちろん、マッチしてしまうプレイヤーとしてはたまったものじゃないですが)。

2021年11月3日水曜日

Radeon RX 6800 XT の OpenCL を有効化する (スクリプト化編)

Radeon RX 6800 XT の OpenCL を有効化する に記述したレジストリ操作について、毎回手作業で実施することが面倒なため Python スクリプトに起こした。

#! python

import os
import winreg
 
base_path = r"C:\Windows\System32\DriverStore\FileRepository"


def detect_amdocl():
    amdocl_list = []
    for curDir, dirs, files in os.walk(base_path):
        for file in files:
            if not file.lower() == "amdocl64.dll":
                continue
            amdocl_fullpath = os.path.join(curDir, file)
            amdocl_timestamp = os.stat(amdocl_fullpath).st_mtime
        
            amdocl_list.append({'path': amdocl_fullpath, 'ts': amdocl_timestamp})

    if len(amdocl_list) == 0:
        return None

    amdocl_list = sorted(amdocl_list, key=lambda e:e['ts'])
    return amdocl_list[-1]['path']


def remove_old_values(regkey, latest_amdocl=None):
    i = 0
    while True:
        value_name = None
        try:
            value_name = winreg.EnumValue(regkey, i)[0]
            i += 1
        except OSError:
            # Will be here when the enumeration is done.
            break

        if not value_name.lower().endswith("amdocl64.dll"):
            continue

        if latest_amdocl:
            if value_name.lower() == latest_amdocl.lower():
                print("latest value: %s" % value_name)
                continue

        print("delete value: %s" % value_name)
        try:
            winreg.DeleteValue(regkey, value_name)
        except Exception as e:
            print(e)


if __name__ == "__main__":
    amdocl_path = detect_amdocl()
    if not amdocl_path:
	    raise Exception("Could not determine path of amdocl64.dll")

    reg = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE, )
    regkey = winreg.OpenKey(reg, r"SOFTWARE\Khronos\OpenCL\Vendors",
            0, winreg.KEY_ALL_ACCESS)

    # Delete old values.
    remove_old_values(regkey, amdocl_path)

    # Add a new key.
    winreg.SetValueEx(regkey, amdocl_path, 0, winreg.REG_DWORD, 0x00000000)

    winreg.CloseKey(reg)

    print("done")
    

PowerShell での実装がより理想的かもしれないが。今度 cxFreeze による EXE バイナリ化などを試してみようと思う。


2021年5月28日金曜日

A Puzzle A Day のソルバーを書いてみた(6/20更新)

更新内容

6/20 ... C言語版について追記しました。

面白そうなパズルとの出会い

Twitter で A Puzzle A Day というカレンダーパズルを見かけた。


このパズルは1月〜12月、1日〜31日までのセルがあり、任意の日付(たとえば5月28日だとか6月1日だとか)をパズルを通して表現できる、というもののようだ。テトリスにでてきそうなブロックを組み合わせて、目的の日付にかかわる部分以外をキレイで埋められれば完成。毎日パズルが楽しめるし、1年後に同じ日付の答えを得ようとしても改めて楽しめそうで、とても面白そうだ。

久々に「このパズルは私も欲しい!」と思った。

国内の販売店が普通に売ってるようなものだったらうっかりオーダーしていたかもしれない。どうやらノルウェーのほうでレーザープリンタを用いて作ってるような感じで、持っている人は少ないのではないか。 Twitter タイムラインをみると紙やら木材のレーザーカットやら思い思いの方法で手作りをして楽しんでいる人もいるようで、私も何か作ろうと思った。

その結果として、ソルバーができた。

https://github.com/hasegaw/puzzle-a-day-solver


ソルバー?

ソルバー(Solver)というのは答えを求めるツールだ。たとえば今回のソルバーの場合、5月28日であればこうすると良い感じにブロックがはまりますよ、というのを教えてくれる。このパズルは365通りの目標があるほか、それぞれに複数の解法があるので、趣味などとしてソルバーを書いてみても楽しいだろうと思って、作ってみた。

私はコンピュータサイエンスを学んだエンジニアでも競プロ勢でもアルゴリズムに強いわけでもないので、ここで紹介するアルゴリズムについてはツッコミどころもあると思う。この記事の内容は、とりあえず趣味で簡単なプログラムを書いているだけでも、このパズルのソルバーはこれぐらいの考えで作れる内容だよ、という紹介ということでご理解いただきたい。


ルールの確認

このパズルのボードは下記の構造になっている。



左が、パズルの舞台となる盤面で、ここにブロックを詰めていくことになる。黒色のセルはブロックが置けない場所で、オレンジ色のセルは、一例として5月28日を表現する場合においてブロックで隠れてはいけないセルだ。

右側に示した 8つのブロックを盤面に並べていく。各ひとつ使えるのだが、各ブロックは回転させても良いし、反転させても良いようだ。なので、ひとつのブロックでも盤面上では最大で8つの姿をとりうる。



盤面の黒色セルおよびオレンジのセルに重ならないように、これらの8つのブロックを配置できたらパズルが解けたことになる。


基本的な考え方

ボードサイズは7x7のマトリックスで表現できる。ここで、ブロックを置いてはいけないセルを 1 、それ以外を 0 としたマトリックスを作る。ルール上絶対にブロックが乗ってはいけない部分のみ 1 としたマトリックスの例、それに加えて目的の日付セルも 1 としたマトリックスの例を示す。



ここで黒色セルと日付セルをわけて考えてもよいのだが、候補を見つけていくには、黒色セルおよび日付セルの両方がフラグされている後者のマトリックスが基点になる。

これに対して、特定のブロックを特定の回転・反転・位置の条件で配置する場合に、どのセルにあたるかを示す行列(以後、候補とよぶ)を作成する。ここではふたつの例を示す。


候補がボードにはまるかどうかは、現状のボードの状況を示すマトリックスと候補のマトリックスを加算してみればわかる。整数値の加算結果として 1 を超えるセルが発生する場合は、ブロックと何かがぶつかってしまい、そこに当てはめられない事を意味する。(ここで、後の最適化で触れる論理積を使ってもいい)


当てはめられる候補を見つけたら、再帰的に他のブロックも当てはまる所を探していって、全てのブロックを使い切ったら解法が見つかったことになる。



実際のところ、これを適当に置いていってもブロックが素直に収まってくれる可能性はかなり低い。黒色セルとオレンジの日付セルを除いて全てのセルをブロックで埋めないと答えにたどり着かないことを利用して、マトリックスのふち側のセルから優先して埋める。これにより、隙間の発生により絶対に解法にたどり着けないことが自明な候補を探索対象から外すことができる。



私の実装では左上から右へ、行が埋まったら次の行へ、という順番でセルを埋めるようにしている。次に埋めるセルを見つけるには、ボードの最新盤面におけるゼロのセルを見つければよい。候補がそのセルを埋めてくれるかは、その候補において目的のセルが 1 になっているかを見ればよい。候補が目的セルを埋めなくても解法に繋がる可能性はあるが、後に別のセルを埋める際に改めて評価されるので、ここでは候補から除外してよい。とにかくフチから埋めて、ボード上に隙間ができない候補から試したほうがよいからだ。

これを繰り返していくと、ボード上のセルでゼロがなくなり(すべて 1 になる)、未使用ブロックもなくなっているはずだ。この方法で総当たりしていけば、解法にたどり付ける。

実装については Pyhthon + NumPy を利用した。 NumPy で上記において必要となるマトリックス演算のほとんどが実現できるほか、この後に触れるベクタライゼーションを意識した記述にすることで、さらなる高速化が期待できる。


ベクタライゼーション (Vectorization)

上記のアルゴリズムは、どのようなプログラムで書いてもよい。私は Python を使用したが、別に Ruby を使ってもいいし、 JavaScript/TypeScript でも、 Perl でも、 Golang でも C でも Pascal でも、 C# でも、 Java でも、 BASIC でもなんでもよい。

ただ、このような処理を愚直にループ処理で書いても、コンピュータはこれを速く処理ができない。このため、コンピュータが高速に処理できるような、まとまった行列演算にできるだけ落とし込んでいく。たとえば盤面に対して次の候補を探すためにマトリックスを加算する場合に、まとめて加算をしたほうが速い。

もちろん加算すべきは ボードの現状 × 候補の数 なので計算すべき量は一緒なのだが、 Python コードとして繰り返しを書くよりも、 NumPy などの C 言語ベースで書かれたライブラリ内でループをしたほうが速い。場合によっては、バックエンドのライブラリ内において、 CPU の SIMD 命令で計算することにより、より実行時間を短縮してくれる可能性がある。

具体的には、 10x10 のボードに対して 10x10 の候補が 10,000 あった場合、 Python レベルで10,000回加算するよりも、 10000x10x10 の候補に対して 10x10 のマトリックスを加算するほうが速く実行できる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import cProfile
import numpy as np

mat1 = np.zeros((10000, 10, 10), dtype=np.uint8)
mat2 = np.zeros((10, 10), dtype=np.uint8)

mat1[:, :, :] = 3
mat2[::, :] = 3


def func1():
    mat1 + mat2


def func2():
    for m in mat1:
        m + mat2


if __name__ == '__main__':
    cProfile.run('func1()')
    cProfile.run('func2()')

% python bench.py
         4 function calls in 0.000 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.000    0.000 :1()
        1    0.000    0.000    0.000    0.000 bench.py:12(func1)
        1    0.000    0.000    0.000    0.000 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}


         4 function calls in 0.005 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.005    0.005 :1()
        1    0.005    0.005    0.005    0.005 bench.py:15(func2)
        1    0.000    0.000    0.005    0.005 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}


また、大抵の場合、 for ループなどを使用し最小値を探すよりも、 NumPy.argmin() などのライブラリ関数を活用したほうが速い。

このような観点でベクタライゼーションをある程度進めたところ、当初手元のコンピュータ(MacBook Pro 2019, 2.5GHz)では 600秒(10分)かかっていた探索が60秒以内で完了するようになった 。


バイナリ化

ベクタライゼーションにより特定の日付向けの解法が60秒以内に得られるようになったが、もう少し速くしてみようかなと思い、内部表現をバイナリ化してみることにした。

ここまでの作業は (7, 7) の大きさのマトリックスで、各要素は 8bit の整数値として扱ってきた。しかし 7x7 というサイズで、上記アルゴリズムでは基本的に 0 と 1 のみ(衝突検出のみ 2 が登場)で成り立っているので、2進数で表現できる。これにより 7x7=49バイトで表現していた盤面や候補が8バイトで表現できるようになり、メモリ上の表現が7分の1になること、uint64(64ビット符号なし整数)表現でのビット演算は現在のプロセッサだと1命令で実行できる。大幅な高速化が望めるだろう。

盤面の各ビットを以下のように割り当てることにした。最上位ビットから利用して最下位ビット側を未使用にしているのは若干気持ち悪く見えるかもしれないが、当初はブロックをシフトした際の範囲外判定のために右側のビットを残しておこうと思ったのでこのような割り当てになっている(結局使わなかったが)。気に入らなければ後述するルックアップテーブルの値を調整すればよい。

追記)このようにビットアサインするつもりだったが、実際には違うビットアサインをしていた。上記のビットアサイン表は参考例としてそのまま掲載しておく。

既存のソースコードの (7, 7) のマトリックスの内部表現からこのバイナリ64ビットの内部表現に変換するため、以下のような関数を定義した。本格的なループ処理の前後で使われるだけなので、かなり適当な書き方だが、ここでの実行時間は大したことないので問題ない。

def generate_mat2bin_lut():
    lut = np.zeros((7, 7), dtype=np.uint64)
    for y in range(7):
        for x in range(7):
            b = 8 * y + x
            lut[y, x] = base_bit >> (63- 8 * y - x)
    return lut

def mat2bin(mat):
    assert mat.shape[0] == 7 and mat.shape[1] == 7
    lut = mat2bin_lut.copy()
    lut[mat == 0] = 0
    return np.sum(lut) # as np.unit64

def bin2mat(b):
    mat = np.zeros((7, 7), dtype=np.uint8)
    for y in range(7):
        for x in range(7):
            if (b & mat2bin_lut[y, x]):
                mat[y, x] = 1
mat2bin_lut = generate_mat2bin_lut()

二進数について理解している人なら(もっといいやりかたがあるよという指摘はいくらでもあるだろうが)何をしているか判ると思うので、詳細は省略する。

これで mat2bin() によりマトリックス表現からバイナリ表現に変換し、 bin2mat() によりバイナリ表現からマトリックス表現に変換できるので、既存コードにある初期値や出力用の関数はほぼそのまま使い回せる。

バイナリ表現化したことで盤面のほか候補についても64bitで表現できるようになったし、いっそのこと、あらかじめ8つのブロックの方向・反転・位置スライドの全パターンをメモリ上に展開しておくことにした。たとえばひとつのブロックの候補は以下のような196のuint64値で表現できる。

000000000000030e 000000000000061c 000000000000070c 0000000000000c07 0000000000000c38 0000000000000e03 0000000000000e18 000000000000180e 0000000000001870 0000000000001c06 0000000000001c30 000000000000301c 000000000000380c 0000000000003860 0000000000006038 0000000000007018 0000000000030e00 0000000000061c00 0000000000070c00 00000000000c0700 00000000000c3800 00000000000e0300 00000000000e1800 0000000000180e00 0000000000187000 00000000001c0600 00000000001c3000 0000000000301c00 0000000000380c00 0000000000386000 0000000000603800 0000000000701800 0000000001010302 0000000001030202 0000000002020301 0000000002020604 0000000002030101 0000000002060404 00000000030e0000 0000000004040602 0000000004040c08 0000000004060202 00000000040c0808 00000000061c0000 00000000070c0000 0000000008080c04 0000000008081810 00000000080c0404 0000000008181010 000000000c070000 000000000c380000 000000000e030000 000000000e180000 0000000010101808 0000000010103020 0000000010180808 0000000010302020 00000000180e0000 0000000018700000 000000001c060000 000000001c300000 0000000020203010 0000000020206040 0000000020301010 0000000020604040 00000000301c0000 00000000380c0000 0000000038600000 0000000040406020 0000000040602020 0000000060380000 0000000070180000 0000000101030200 0000000103020200 0000000202030100 0000000202060400 0000000203010100 0000000206040400 000000030e000000 0000000404060200 00000004040c0800 0000000406020200 000000040c080800 000000061c000000 000000070c000000 00000008080c0400 0000000808181000 000000080c040400 0000000818101000 0000000c07000000 0000000c38000000 0000000e03000000 0000000e18000000 0000001010180800 0000001010302000 0000001018080800 0000001030202000 000000180e000000 0000001870000000 0000001c06000000 0000001c30000000 0000002020301000 0000002020604000 0000002030101000 0000002060404000 000000301c000000 000000380c000000 0000003860000000 0000004040602000 0000004060202000 0000006038000000 0000007018000000 0000010103020000 0000010302020000 0000020203010000 0000020206040000 0000020301010000 0000020604040000 0000030e00000000 0000040406020000 000004040c080000 0000040602020000 0000040c08080000 0000061c00000000 0000070c00000000 000008080c040000 0000080818100000 0000080c04040000 0000081810100000 00000c0700000000 00000c3800000000 00000e0300000000 00000e1800000000 0000101018080000 0000101030200000 0000101808080000 0000103020200000 0000180e00000000 0000187000000000 00001c0600000000 00001c3000000000 0000202030100000 0000202060400000 0000203010100000 0000206040400000 0000301c00000000 0000380c00000000 0000386000000000 0000404060200000 0000406020200000 0000603800000000 0000701800000000 0001010302000000 0001030202000000 0002020301000000 0002020604000000 0002030101000000 0002060404000000 00030e0000000000 0004040602000000 0004040c08000000 0004060202000000 00040c0808000000 00061c0000000000 00070c0000000000 0008080c04000000 0008081810000000 00080c0404000000 0008181010000000 000c070000000000 000c380000000000 000e030000000000 000e180000000000 0010101808000000 0010103020000000 0010180808000000 0010302020000000 00180e0000000000 0018700000000000 001c060000000000 001c300000000000 0020203010000000 0020206040000000 0020301010000000 0020604040000000 00301c0000000000 00380c0000000000 0038600000000000 0040406020000000 0040602020000000 0060380000000000 0070180000000000

この値を bin2mat でデコードしてみるとブロックが見えてくる。

% python
Python 3.8.1 (default, Mar 12 2020, 17:27:26)
[Clang 11.0.0 (clang-1100.0.33.17)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import puzzle_a_day_solver
>>>
>>> puzzle_a_day_solver.bin2mat([0x000000000000030e])
array([[0, 1, 1, 1, 0, 0, 0],
       [1, 1, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
>>> puzzle_a_day_solver.bin2mat([0x000000000000061c])
array([[0, 0, 1, 1, 1, 0, 0],
       [0, 1, 1, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0]], dtype=uint8)

8ブロック合計では、重複を除くと合計1196パターンあり、正味9568バイトのデータ量に収まるので、全てのパターンを予め展開しておいても、Core 2 DuoのプロセッサでももしかするとL1キャッシュに収まってしまうかもしれないデータ量だといえる(プロファイリングしていないので実際にどうなっているかは判らない)。

ブロックの衝突チェックは、従来方法では足し算をしていたが、今回は論理積(AND)を使用する。盤面 AND 候補を計算した結果、重複ビットがあるとゼロ以外の値になるので、論理積の結果がゼロ以外の候補は除外する。ここでも NumPy による vectorization を行う。

    blocks_f2_masked = blocks_f1_uint64 & mat_uint64
    blocks_f2_bool =blocks_f2_masked == 0
    blocks_f2_uint64 = blocks_f1_uint64[blocks_f2_bool]

特定セルを埋めてくれる候補かどうかを判断するためには、特定セルを示すビットが 1 である候補に絞り込めばよい。 mat2bin/bin2mat 向けに作った、各セルに対するビットを示すルックアップテーブルから 64bit のビットマスクを得て、先と同様にフィルタリングする。

    bit_pos = mat2bin_lut[pos[0], pos[1]]
    blocks_uint64_masked = blocks_uint64 & bit_pos
    blocks_f1_bool =blocks_uint64_masked != 0
    blocks_f1_uint64 = blocks_uint64[blocks_f1_bool]

基本的なアルゴリズムは変えずに内部表現だけ変更しているので、得られる結果は一緒だが、49秒かかっていた処理が3.5秒まで短縮できた。

単純な計算部分だけで考えればもっと速くなる(1秒は切れるだろう)と思っていたので、思ったより遅かった。



C言語で再実装してみた

翌週、実はC言語で再実装してみた

もうこれ以上は最適化なんてしないって思っていた。でも1秒を切れなかったのがやはり心残りだったんだと思う。

基本的なアルゴリズムは Python バージョンと一緒だが、本質的ではないところで 楽をしたり、異なる実装をしたりている。

まず Python バージョンでは盤面の制約やブロックの形状を配列として表現しておき、実行時に 64bit のバイナリ表現を得ていた。 C 言語でこれを書き直そうとするとちょっと面倒なので、 Python バージョンが得られたバイナリ表現をソースコード中にLUTとして埋め込みしておく。

ブロックのLUTと、ブロックのメタデータ(LUT内の開始位置と数量)を独立したデータとして持っている。各ブロックの大きさや形状の違いからブロック数量が異なる。このためLUT自体はひとつ一次元配列で持っているので、CPU L1キャッシュを無駄にしない。

現状のコードは日付の指定や結果の出力機能を持っていない。 Python バージョンと同じデータ構造なので、 Python モジュールとしてコンパイルしてしまい、 Python バージョンを宿主として足りない部分は既存コードを使い回そうかなと思っている。

C言語で書くと有り難いのが次に攻略すべきセルを選択する部分で、ここはどうしても Python 上で速くする方法を思いついていなかった。いくつかのパターンを試した感じでは NumPy に任せておいたほうが速かったので、そうなっていた。

# バイナリ化前
def next_cell(context):  mat = context['mat'] pos = np.unravel_index(np.argmin(mat), mat.shape) minval = mat[pos[0], pos[1]] if minval != 0: return None return pos
# バイナリ化後 def next_cell(mat):
mat = mat2bin_lut & mat pos = np.unravel_index(np.argmin(mat), mat.shape) minval = mat[pos[0], pos[1]] if minval != 0: return None return pos

この部分はC言語であれば素直に書ける。

/* 盤面 board に対して次のセルを選んで、その bit の値を返す */
inline uint64_t next_cell_bit(uint64_t board) {
    for (int y = 0 ; y < 7; y++)
        for (int x = 0 ;x < 7; x++) {
            uint64_t done = board & mat2bin_lut[y][x];
            if (! done)
                return mat2bin_lut[y][x];
        }
    return 0;
}

ある時点のC言語バージョンでは gcc の最適化なしで 40ms (0.04秒)、 -O3 で 20ms まで実行時間を短縮できる事を確認できた。M1 Macでは10msで完了したという報告ももらった(速っ!)。現時点のものは、もう少し最適化が進んでいる。


キャッシュミス分析

このソルバーは約10KBのLUTを使って、最大8回の再帰呼び出しするだけなので、この規模であれば、ここ10年のプロセッサーではCPUのL1 Cacheに載りきってしまうことが想像できる。以下は Ryzen 9 3900X の Linux box において cachegrind でこのソルバーをプロファイルしてみた結果である。

$ valgrind --tool=cachegrind ./solver
==114519== Cachegrind, a cache and branch-prediction profiler
==114519== Copyright (C) 2002-2017, and GNU GPL'd, by Nicholas Nethercote et al.
==114519== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==114519== Command: ./solver
==114519==
--114519-- warning: L3 cache found, using its data for the LL simulation.
50 solutions found
==114519==
==114519== I   refs:      100,596,976 .... 命令の参照数
==114519== I1  misses:          1,140 .... L1 キャッシュミス
==114519== LLi misses:          1,116 .... L3 キャッシュミス
==114519== I1  miss rate:        0.00%
==114519== LLi miss rate:        0.00%
==114519==
==114519== D   refs:       19,428,245  (18,233,828 rd   + 1,194,417 wr) .... データ読み書き数
==114519== D1  misses:          3,401  (     2,718 rd   +       683 wr) .... L1 キャッシュミス
==114519== LLd misses:          2,817  (     2,213 rd   +       604 wr) .... L3 キャッシュミス
==114519== D1  miss rate:         0.0% (       0.0%     +       0.1%  )
==114519== LLd miss rate:         0.0% (       0.0%     +       0.1%  )
==114519==
==114519== LL refs:             4,541  (     3,858 rd   +       683 wr)
==114519== LL misses:           3,933  (     3,329 rd   +       604 wr)
==114519== LL miss rate:          0.0% (       0.0%     +       0.1%  )

I1 miss rate および D1 miss rate が 0.0% となっていることで、プログラム自体もデータも L1 キャッシュに収まっていることがわかる。念のためソルバーの実行回数を増やしたものを同じくプロファイラにかけてみたりもしたが、大きくキャッシュミスが増えたり、キャッシュミス率が上昇したりする状況を確認できなかった。

LLd miss rate の 2213 rd は恐らく 64bit のキャッシュラインへの読み込みが2213回発生したということだと思うので、 2,213*8=17,704 [bytes] で、コード中で使っている LUT サイズが 10KB 前後、あとはヒープや自分が書いたものではない関数内のメモリ操作をあわせると、こんなものなんだなと思っている。

知識として CPU のキャッシュの事は知っていたけども、こういったデータを具体的に見る機会は今までなかったので、パズルついでにここまで楽しめるのはお得感があった。


これで終わりだと思うけど


一度「もうこれ以上はやらない」と書いていたのに色々と手を入れてしまったので、ほかにも思いついている最適化を試してしまうかもしれない。
  • mat2bin_lut の unroll
    試した範囲では認識できるほどの差がないことがわかっている
  • mat2bin_lut を引くのをやめて毎回計算で求める (のとどちらが速い?)
  • AVX512


おわりに

とりあえず数日間でソルバーを書いて遊んでみた内容をまとめてみた。

A Puzzle A Day のパズルはアナログでありながら、365日飽きもせずに遊べそうで、とても面白いと思う。同時に盤面は 7x7 未満とかなり小さく、私みたいに総当たりしてもそれほど難しくないので、プログラミング経験が浅くてもソルバーを実装にチャレンジするにはお手頃な問題ではないかと思う。


これまで使ってきた Mac を回想する

私が普段使いのコンピュータを Mac に変えたのは 2007 年のことだった。知人が悪名高き MacBook Air (初代で何もしなくてもCPUスロットルするようなひどい世代)を可愛がることに決めて、私に 2006 年モデルの白色プラスチックモデルの MacBook を譲ってくれたので、 Mac に乗り換えることにした。 CoreDuo, 4GB RAM とかの時代のモデルで、快適に使えていた気がする。後述の 2008 年モデル購入後に、さらに知人に引き取られていった。

時間軸としてはこちらのほうが前なのだが、もともと 2004年頃に G4 Mac を購入して持っていて、まあ16年以上前のことだから書いてしまってもいいかな。まだ所属会社のセキュリティポリシも緩かったので、会社への申請を通して自分の G4 Mac を会社のデスクに設置、社内ネットワークに接続して、いつもロードアベレージが高いメールサーバからメールを吸い上げて、 imapd を動かして最強のメールスプールとして使っていた。数年後には社内のセキュリティポリシの変化に伴い、社内にもっていた私物 PC (ほかに自作FreeBSD PCのIRCサーバなども置いていた)は撤収することになった。

新品で買った MacBook は 2008 年モデルの 13 インチで、この個体では FreeBSD のソースコード読み込んだり、勉強会などに参加したりでかなり使い込んだ。仕事用 Windows PC は Cygwin, xinetd などを組み合わせ、普段使いは gVim な環境になっていたが MacBook 上は普通に UNIX ですっきりした環境でよかった。今でも手元に残ってはいるけど、通電はほとんどしていない。

2011年、転職のタイミングで MacBook Air 13インチを購入してメインPCにした。外回りのセールスになって、会社からの支給コンピュータは MacBook 13 インチだったが、一日に 3 アポとか都内を走り回る業務スタイルにおいてあの重さのコンピュータを常に携帯するのは負担だったので、自分でコンピュータを持ち込んでいた。当時は国内に社員が3~4名しかいない会社でやり放題(?)していたが、いまだと許されないかもしれない。

翌年の MacBook Air 2012 13インチを買ったのは、上記 Air のハードウェア故障で修理に出している間にコンピュータがないのは困るということで、買い換えた。当時の Air はフルスペックでも20万しなかったし、ノート型コンピュータをとても酷使していたので、1年しか使ってなくても買い換えていいかなというぐらいの気持ちで買い換えた。 2011 年モデルと比べると内蔵グラフィックスの性能向上がすごくて、良いコンピュータだった。

この頃の Mac はリーマンショック後の円高の影響で、スペックの高い個体がかなり安く買えていたこともあって、エンジニアにとっての Mac は安くてコスパのいい使い捨てマシンだったという感じがあった。

退職後、 MacBook Pro 2014 15インチを購入。 Pre Type-C 世代としては2015年モデルが最強だったが、 2014年モデルもハードウェア、ソフトウェア的に非常がバランス取れていて良いコンピュータだった。IkaLogの開発に使用したのもこのコンピュータである。冷却機構に埃がたまりサーマルスロットリングの病気が発生、掃除が必要だったが、当初はそれぐらいのトラブルで、とても快適な個体だった。残念ながら、手元の個体はディスプレイやキーボードの破損などが度々発生し、維持しつづけても負債になると考えて、保証切れとともに手放すことにした。30万以上したが、15万円ぐらいで売れた。

2017年に現職に入った際、 MacBook Pro 2016インチをメインマシンにして、ここでは(いまごろ私が改めて言う話でもないが)バタフライキーボード等のデキが悪くて正直かなり使いづらいコンピュータになっていた。とはいえソフトウェア面での仕上がりは悪くなくて、 MS OneNote が同期に失敗しはじめると無限に CPU 時間を消費してとネットワークトラフィックを吐き続けることぐらいだったと思う。

なおこの MacBook Pro 2016 、 2019年の AppleCare 保証が切れ&リースアップ前にキーボードの保守交換(在庫の都合上UK->US)でめちゃくちゃ良いマシンになったと思う。酷評されていたバタフライキーボードも、改善版になってからはびっくりするほど入力しやすくなった。

キーボード交換の際、バッテリ交換、本体シャーシ交換、温まったときにクラック音が生じていた問題があり液晶パネル交換となり、システムボード以外は新品同様になったのでこのまま暫く使い続ける選択肢も考えたのだが、過去の経験上 Mac を修理なしで使い続けられるという感覚がなく、使い続けても故障のタイミングで想定外のキャッシュアウトを会社に負担させるのもどうかと思ったので、リースアップの際に泣く泣く手放した。

2019年、本当はノート型コンピュータなんて1台だけ持っていれば十分だと思っていたのだが、情シスなどと相談したうえで海外出張時の PC を普段使いと分けることになって、 MacBook Air 2019 を併用しはじめた。正直、あまり使っていないし思い入れのない個体。もっと使ってくれる人のところに行けたら、よかったのにね。のちにコロナウイルスの蔓延もあり、本当にほとんど出番がない。

いま在宅で普段使いしているコンピュータは MacBook Pro 2019 16インチで、キーボードのタイプ感がまた変化した。個人的には2016年の改善版のバタフライキーボードでもよかったかなと思うけど、まあ過去には戻れないので仕方がない。コロナウイルス禍にあり家から出ない日も増えたし、 MacBook Pro 2019 16インチは最強のリモートワーク母艦となっている。

ただ、この普段使い MacBook Pro 2019 、またOSのアップデートが増えてから、いまいちOSとして安定しなくなってきた。ハードウェアとしては気に入っているんだけども、ソフトウェアとのバランスが微妙になったのかなーと感じている。

「インストールしているソフトウェアや接続しているハードウェアが悪いんじゃないか」「俺のMacは暴れねえよ」などとは度々言われるのだが、なぜかうちの Mac はどうしても暴れてしまう。ここ1年ぐらいで kernel_task の CPU 時間消費が極端になってきて実用に耐えないレベルになりつつあるので、仕事する上で困るし、対策を打たないといけない。

自分にとっていちばん Mac が使いやすかったのは、 2012年~2015年のことだった。2017年以降、新しい Mac を手にする際には毎回「これが自分にとって最後の Mac になるかもしれない」と思っている。

2021年4月10日土曜日

高リフレッシュレート対応4K液晶ディスプレイ ASUS ROG Strix XG27UQ を購入した

2022/6 追記

本記事は 144Hz 4K ディスプレイの選択肢がまだほとんどなかった頃に XG27UQ を購入した際の内容です。

どうしても DisplayPort 入力が2ポート欲しい、 4K 144Hz 仕様としては割安なディスプレイが欲しいという理由であれば XQ27UQ を候補になると思いますが、そうれでなければ、現在では LG 27GP850 をはじめ複数の HDMI2.1 対応 4K ディスプレイが流通しています。これから検討される方は他の 4K 144Hz ディスプレイも確認されることをお勧めします。

 ~~


高リフレッシュレートなディスプレイに買い換えようと思って以来半年近くたち、ようやく本命のディスプレイが手元に届きました。



今回のXG27UQは2021年2月に国内向けに発表されたモデルで、これまで秋葉原などでお店を回っても実物を見かけることはありませんでした。発表当初にすぐ予約された方は2月中に入手できたようですが、4月に入ってようやく実物が届いたぐらいで、現時点で欲しくても実物を見られる方は限られていると思います。このモデルは実売で10万円近くしますし、「失敗したくないので少しでも多く情報が欲しい」という方もいらっしゃるかと思います。このため、所感をまとめておくことにしました。

調べてみると、メーカーからテスト機を借り受けたブロガー等がすでにレビュー記事を投稿されていますし、そういった方でカバーされている分については、そういった記事にお任せすることにしました。私の記事では、他の記事ではあまり触れられていない部分について触れていこうと思います。


ファースト・インプレッション編

マニュアル冊子が付属していない

本製品は付属品として製品保証についての冊子、1枚っきりのクイックスタートガイド、およびキャリブレーションレポートが付属していますが、マニュアルは同梱されていません。スタンド部分を外す方法がわからずに困っていたところ、 ASUS のサイトにはマニュアルが PDF で存在していることがわかりました。

https://dlcdnets.asus.com/pub/ASUS/LCD%20Monitors/XG27UQ/XG27UQ_Japanese.pdf

マニュアル冊子なんて一度使い始めたらほとんど見返すこともないのは事実ですし、意図的に省略されているのでしょう。とはいえ、 10 万円近いモニタですし、今回の製品はマニュアルを見ないとよく判らない機能やギミックも積まれているモデルです。紙媒体でマニュアルが付属されていたり、もしくは少なくともURLや検索キーワード、QRコード等でもよいので、マニュアルへの案内があると良かったなと思いました。


日常の利用編

複数ソースの切り替えが面倒。ファームウェア更新による改善を期待

XG27UQ は DisplayPort 2入力、 HDMI 2入力で合計4入力利用できる点がひとつの特長です。ディスプレイです。しかし、ディスプレイの入力を切り替えるためには画面裏側右手の赤いスティックを使い、クリック→下→下→下→右と操作し、その上で入力ソースをスティックで選択する流れになっており、煩雑です。

赤いスティックの下にはゲーミングモードを設定する画面へのショートカットがふたつあります。これらのボタンで、GamePlus設定画面、およびGameVisual設定画面への直接遷移できるようになっていますが、たぶん、私は、どちらもほとんど押す機会がないでしょう。これらのボタンから入力ソースの切り替え画面へ遷移できるような設定が可能となるファームウェアアップデートに期待したいです。複数の入力ソースを接続する方の多くは、同じような感想をお持ちになるのではないでしょうか。


USB3.0ハブ機能は、ディスプレイの主電源と連動/非連動が選べる

XG27UQ は USB3.0 の2ポートUSBハブ機能を搭載しています。私の場合、ダウンストリームには、ディスプレイの付近にあるデバイスとして、Webカメラとして利用している一眼デジタルカメラ(Canon EOS Kiss X6i)とアイトラッカー(Tobii Eye Tracker 5)を接続しています。ディスプレイ自体にこの機能を求めてはいないのですが、セットアップがすっきりした点についてはメリットを感じました。


このUSBハブ機能はディスプレイ機能と独立して動きますが、ディスプレイの主電源がオンの状態でのみUSBハブが動作する連動設定と、ディスプレイの主電源の状態にかかわらず外部電源が供給されていればUSBハブが動作する非連動設定のどちらかを選ぶことができます。

工場出荷時は主電源と連動する設定になっていますが、私の場合で常時通電のデスクトップPCとの組み合わせで利用するため、USBハブが常時動作する設定に切り替えました。


ディスプレイアームとの組み合わせ利用について

XG27UQ はディスプレイアームへの取り付けに対応しています。ただし、アームの取り付け時に標準スタンドの取り外し方がすぐに判からず、マニュアルを見ながら試行錯誤したほか、この機種特有の考慮点があるので、ここで紹介したいと思います。



標準スタンドの取り外し方

この作業には、化粧パーツの取り外しにマイナスドライバー、M4ネジを付け外しするためのプラスドライバーが必要です。

工場出荷時にはスタンド用の足が取り付けられた状態になっています。足のまわりにある半リング形状の化粧パーツを取り外します。スタンドの裏を覗き込むとマイナスドライバーを差し込める隙間があるので、ここにマイナスドライバーを差し込んで化粧パーツを引き上げます。

パーツが浮いてきたら、半リング形状の化粧パーツ2点を、ディスプレイ本体から平行に離すように取り外します。取り外し時に多少力をかける必要がありますが、樹脂パーツ自体は十分な強度があるので、丁寧に外せば問題ありません。この作業を終えると、VESA規格のM4ネジが4本露出します。

足のまわりに見えてきたVESA規格のM4ネジを4本はずすと、標準スタンド用の足がはずれて、ディスプレイアームを取り付け可能な状態となります。

半リング形状の化粧パーツは取り外したスタンドに取り付けられるので、保管時にはスタンドにはめて保管しておくとよいでしょう。


VESAマウントのネジ穴は25mm の窪みの中にある

ディスプレイアームの種類によっては、カメラのクイックシューのように、あらかじめディスプレイ側に小さな部品を取り付けておいて、設置済みのディスプレイアームに固定できるような機能をもった物があります。このような場合VESA標準のネジ穴のまわりに十分なクリアランスが必要です。また、スタンドを非純正品に交換した場合にも注意が必要なポイントでしょう。

今回は colebrook bosson saunders 製の Wishbone ディスプレイアームに取り付けるつもりでいたので、この部分は気になっていましたが、結果的には十分なクリアランスがあり、Wishbone アームに対して、問題なく載せたり外したりできることが確認できました。 


ディスプレイアーム利用時、ビデオ用ケーブルを完全に隠せない

XG27UQ は下側から DisplayPort/HDMI などのケーブルを差し込みます。標準スタンドを利用する場合にはあまり気にならないでしょうが、ディスプレイアームに載せる場合は、ビデオ用のケーブルが正面から見えてしまう可能性があります。

がんばって隠すことも不可能ではないのですが XG27UQ は DisplayPort 1.4 に対応した 4K 144Hz 対応のディスプレイで、これを利用する場合には DisplayPort 1.4 に対応した比較的太いケーブルを使用することになるのではないでしょうか。

しかし、上向きに刺さった太いケーブルをディスプレイの裏側で片付けるだけの十分なスペースがないため、正面から見たときには、ビデオ用のケーブルはディスプレイの下側に少し見えるぐらいのセッティングに落ち着くかと思います。もちろん気合いで見えないように隠してもよいでしょうが、アームを動かしたときにケーブルやコネクタに負担がかからないようにある程度の遊びも必要ですし、この点は諦めることにしました。


その他編

当然ながら 4K 144Hz には高品質なケーブルが必要

これまで、2012年に購入した 7m の DisplayPort 1.2ケーブルでPCとディスプレイを接続し、4K 60fpsで動作させていました。このケーブルを利用していたのは諸事情により「手元で余っていた」というのが最大の理由です。(今でも未使用の新品が1本手元にあります...)

このケーブルでも 4K 144Hz はいちおう映ったのですが、不定期に画面がブラックアウトする現象が発生しました。恐らくケーブルの品質的に映像信号が時々乱れているのだと想像できます。

FullHD 144Hz に対して 4K 144Hz は時間あたりに表示するピクセル数が4倍になります。このためビデオケーブルの中を流れる信号の速度が上がっています。 Wikipedia の DisplayPort ページで調べてみたところ、 4K 144Hz は DisplayPort 1.4 仕様からの仕様で、ケーブルの中を 25.92Gbps の信号が流れているようです。 1.2 の時代は 17.28Gbps だったので、データの転送量が1.5倍になっていて、恐らくそれだけDisplayPortケーブル内のビットクロックも上がっているようです。

ディスプレイに付属する DisplayPort 1.4 ケーブルを利用することも考えましたが、手元では昇降デスクやディスプレイアームを利用しており、 3m 程度のケーブル長を必要としていたので、 3m の DisplayPort 1.4 対応ケーブルを購入して交換しました。

また、ビデオカードの DisplayPort コネクタの片方が緩いようで、本体への衝撃ですぐブラックアウトしてしまう事があったので、もう片方の DisplayPort コネクタに装着して安定を確認しました(ビデオカードのメーカー保証で交換依頼をしてもいいかもしれませんが、手間を考えるとこの対応でいいかなぁと思っています)。


MacBook Pro 16インチ (2019) から HDMI で 4K 60Hz で安定出力できている

MacBook Pro から 4K 解像度で出力する際になかなか安定しないイメージを持っていましたが、XG27UQ では HDMI ケーブルを接続するだけで 4K 60Hz で出力された点が拍子抜けでした。ここ一週間ほど、ノートラブルで動作しています。

LG 社のディスプレイでは 4K 60Hz を受け付けるためには追加の設定(DeepColor)が必要だったりして、この設定を行うと映る時と映らない時があるなどしてストレスを感じたので DisplayPort 接続による 4K 60Hz もしくは HDMI 接続による 4K 60Hz を使い分けていました。

不都合が感じたら DisplayPort での 4K 60Hz に切り替えることを考えるでしょうが、実用上問題を感じないうちは、試しに HDMI の 4K 60Hz で使ってみようかと思います。


まとめ

XG27UQ は、4系統入力の4K解像度・高リフレッシュレートディスプレイとしては期待通りの仕上がりだと感じています。

一点だけ不便な点を挙げるとすれば、入力系統の切り替えに必要な操作数が多いことです。この部分は4系統入力を魅力に感じて購入するユーザーにとっては非常に重要なポイントです。もしエンドユーザ側でファームウェアアップデートが可能な仕組みが用意されているのなら、後からでも改善できる点ですから、ASUSには是非検討を御願いしたいところです。

これまで液晶ディスプレイといえばシャープ、LGなど自社で液晶パネル技術を製造できるメーカーの製品を利用していました。今回のASUS ROG XG27UQは実売価格で10万円近くするモデルで、下手な4Kディスプレイの2倍以上の価格です。ASUS自体は液晶パネルを作れるわけではないので買ってきている(というか製品規格だけで、他メーカーに仕様を指定して生産させている可能性も高い)メーカーですので、正直に言えば、そういったメーカーの製品を選定すること自体に抵抗もありました。最初の一週間から「買わなければ良かった」みたいな悪い印象にもならず、ホッとしています。

To Be Continued

フォトトランジスタを使って応答速度などをの特性を測定しているため、結果がまとまったら続編としてまとめたいと思います。

2021年4月8日木曜日

Radeon RX 6800 XT の OpenCL を有効化する

Radeon RX 6800 XT で OpenCL プログラムを動かそうとすると、OpenCL プラットフォームとして同 GPU が列挙されてこなかったので、調べたら、レジストリをちょっと弄る必要があるっぽい。どうやら OpenCL プログラムはこのキーから OpenCL プラットフォームの実装を見つけてるようだ。


Radeon のドライバをインストールもしくはアップデートしたタイミングで、以下のようなディレクトリ、ファイルができる。

C:\Windows\System32\DriverStore\FileRepository\*.inf_amd64_*\amdocl64.dll


amdocl64.dll のフルパスが確認できたら、 regedit.exe を起動して、 コンピューター\HKEY_LOCAL_MACHINES\SOFTWARE\Khronos\OpenCL\vendors に、以下のキーを作成する。

  • 名前: 上記 amdocl64.dll のフルパス
  • 種類: REG_DWORD
  • データ: 0x00000000 (0)
このキーを作成した後に OpenCL を利用するプログラムを実行すると、 Radeon GPU が OpenCL デバイスとして認識される。もし認識されない場合は amdocl64.dll へのパスがおかしいとか、過去のドライババージョンの amdocl64.dll へのパスになっていないかを確認するとよさそうだ。

手元で Radeon のドライバを更新するたびにコレでひっかかってる気がするが、どうしてドライバのインストール時に自動的にやってくれないのかねえ。何も見ずに何すればいいか思い出せるほどの頻度でやることではないが単純作業ではあるので、いっそのことツールなど書いて自動化すると幸せかもしれない。。。

2021年3月14日日曜日

ゲーミングディスプレイは本当に役に立つのか?

2021年にもなって、久々にFPSとしてApex Legendsをプレイしている。ヘタクソなのはそうなのだが、恐らくこの半年間でPS4 Pro/PC版あわせて700~800時間はプレイしているはずだ。久々にFPSをプレイしているので、ゲーミングディスプレイを試して、比較してみることにした。

ゲーミングディスプレイに対する謳い文句への懐疑感


これまで LG 27UD55-BK と LG 27UL850-W (どちらも 27インチ4K液晶)をデュアルディスプレイにしていて、このうち、 2020 年に購入した 27UL850-W をゲームプレイに使用していた。このディスプレイはどちらかと言えば発色などがキレイなことがウリだ。

もちろんリフレッシュレート60Hzの液晶ディスプレイが十分だとは思っていなかった。もともと私は初めてのFPSといえば液晶ディスプレイが普及する前である1990年代の初代DOOMだし、また2000年前後は三菱のダイアモンドトロン管ディスプレイ RD17V で、100Hz 程度のリフレッシュレートでFPSを遊んでいた経験があるので、液晶ディスプレイかつ60Hzが大きな制約であることは理解していた。敢えていえば液晶になる前なら100Hz程度は当たり前だった。また最近のトレンド Oculus Rift などのヘッドセットを見ていても、60Hz では不足だが 90Hz もあれば脳を騙せることが知られているので、60Hzで足りなくても、100Hzぐらいの頻度で更新されれば十分だろうと思っている。

しかし、周りのプレイヤーからは「60Hzよりも高リフレッシュレートのほうがよい」だけで終わらず、「敵レジェンドが止まって見える」「ディスプレイを変えるだけでキル数が増える」などという主張をされるし、販促ビデオでは相手より素早く反応できると謳われている。果たして、そこまでの効果があるのだろうかと、懐疑的に思っていた。

買おうと決意したものの、欲しいものの在庫がない


とはいえ、そろそろゲームに適したモニタが欲しくなってきた。コロナ禍でほとんど外出することもなく、スノーボードは2シーズン連続でスルーしており、月々の大きな支出としては家賃とメシ代(自炊するようになったため2年前の1/4〜3/1ぐらい)ぐらいなのだ。この半年で FPS を500時間以上プレイしているのだから、多少投資してもいいのではないだろうかと。

実は 11 月頃にゲーミングディスプレイの購入を決意したが思ったようなモデルがなく、2月に ASUS が発表した XG27UQ (4K 144Hz)をバックオーダーしている状態である。手元の古いLG 27UB55(27インチ 4K)で何十秒も残像が出る不具合が出ているので、買い換えるなら、同じ利用感で仕事にも使えるゲーミングディスプレイを、と思い、このモデルにたどり付いた。

しかし、昨今の自動車をはじめとする部品不足問題のためか中々納期が確定せず、少なくとも3月中にも手に入らないことがほぼ確定的になった。この機種に限らず、ある程度スペックや型番にこだわりを持って液晶ディスプレイを購入しようすれば、流通量の少なさにすぐ気づくだろう。正直なところ現時点で表示されている納品時期が守られるのかも判らない。

とりあえず、繋ぎのつもりで買ってみた


今回は MSI Optix G241 を購入してみた。スペックを妥協して市中在庫から選べば3万円でお釣りがくる価格感だ。半年近く「ゲーミングディスプレイの在庫がネェ……」と考える日々が続いていたことを考えたら、さっさと買ってしまったほうが精神的に良かったのではないかとさえ思える。

中途半端なものを買いたくなかった最大の理由は、設置場所の都合だ。27インチ×2+MacBook Proでギリギリだったが、やむを得ず、なんとか3枚目のディスプレイを追加した。ディスプレイアームに載せてあるので利用シーンによってある程度片付けたり引っ張り出したりはできる状態にある。

とりあえずの印象


早速 Optix G241 を Windows PC に繋げてみると、マウスカーソルの動きがかなり滑らかになった(というか見える残像が増えた)のはすぐわかり、印象的だった。ウインドウのドラッグなども全体的に滑らかになっているのもわかる。

Forza Horizon 4 を動かしてみると 60Hz と 144Hz では高速道路を高速巡航しているときに横を流れていく鉄柱の滑らかさが印象的だった。

過去に多少試したことがある Aim Lab を改めて試してみると、これまで 30,000 点ちょっとの印象だったテストで 50,000 点台が出てきて驚いた。ただ、ディスプレイの変更だけでなく自分のプレイスキルの向上もあると思うので、そこは考慮しないといけない(特に最近は Apex Legends の仕様アップデートに伴いウイングマンでの練習をはじめている)。


ゲーミングディスプレイによるメリットはこの時点である程度体感できていたが、もう少し定量的に比較してみることにした。

比較環境のセットアップ


今回は GPU (Radeon RX 6800 XT)から、これまで使っていた 27UL850-W (60Hz 4K 5ms) と今回の Optix G241 (144Hz FullHD 1ms) を DisplayPort で同時接続し、 Windows の機能で両方のモニタに映像をミラーする。なお、 Windows の機能で画面をミラーした場合に遅延が増加するかどうかは判っていなくて、この部分を定量的に測定する方法を持っていないので、この観点については無視する。

フレッシュレートは各ディスプレイの最大値が適用されているので、従来のディスプレイでは 60Hz 、ゲーミングディスプレイでは 144Hz で表示される状況となる。これで、どちらを見るかだけで利用できるディスプレイをスイッチできる環境ができた。

定量的に比較できるように、引き続き Aim Lab を使って、プレイ中のディスプレイの表示状況をデジタルカメラのビデオ撮影機能で 60FPS 撮影し比較することにした。ディスプレイの前に三脚を立てて、富士フィルムのX-T3カメラで両方のディスプレイを撮影できるようにした。ひとつのカメラのセンサで両方のディスプレイの出力映像を記録することで、1コマ単位で見比べて比較できる。

もちろん、本来 60Hz と 144Hz の違いを検証するのなら 60FPS では不十分だ。少なくとも 144Hz の倍の 288Hz(FPS) 前後のフレームレートで撮影したほうがいいだろうが、私はそこまでの機材は持っていないから、手持ちの機材でできる範囲での検証とする。


写真左側が LG 27UL850 、右側が MSI Optix G241だ。

なお、正面からの撮影ではないとはいえ、この写真で見ればわかるとおり、発色については Optix G241 に対して 27UL850 のほうが圧倒的にキレイだ。手元で試している限り、 27UL850 と比較できる環境で見れば、 Optix G241 は全体的に色褪せて見える(色温度などの設定でカバーできる範囲ではない)。これらは両方とも IPS パネルだ。


応答速度を比べてみる


とりあえず動画撮影をしながら、両方のモニタで Aim Lab を比較してみたところ、今回購入した G241 のほうがボヤケが明らかに少ないことがわかった。このため目が疲れにくい印象を受けた。これは動画からフレームを切り出してみても判る。


この写真では下にあったターゲットをエイムしてから上にあるターゲットに移ろうとしているが、その際に27UL850ではひとつ前のフレームが残像として映っている。実物では、この残像がボヤけに見えている。Optix G241では、この残像感がとても抑えられている。60Hz 5msのスペックを謳うディスプレイの残像は、デジタルカメラを用いて60フレーム/秒で撮影したレベルでも十分に記録できるほどだった。

表示遅延を比べてみる


録画を確認するとOptix G241のほうが常に若干新しいフレームが描画されていると感じられる。実際には144Hzのリフレッシュレートで更新されていることもあり、操作に対してダイレクトにフィードバックがかかるので、エイム操作がしやすくなる感覚はあった。

動画の各フレームを確認していくと、以下のフレームが見つかった。28UL850では過去のフレームに対して新しいフレームが表示されようとしており、Optix G241ではより新しいフレームが表示されている。


どちらのディスプレイでも、いちばん右のターゲットにはこれから表示される武器の陰がかぶっているが、このかぶり方が非常に似ているのは面白いポイントだと思う。144Hzのほうが明らかにより新しいフレームが表示されている。しかし、60Hzモニタは更新頻度が低いとはいえ、特別に遅延が大きいわけではないようだ。

遅延がどれぐらいあるかはカウントダウンの瞬間などを比較するとわかりやすい。5,4,3...とカウントダウンがなされる時の60Hzディスプレイでの描画の遅れは、表示開始までの遅延は1フレーム(16.6ms)前後、表示が安定するまで2フレーム(32.2ms)未満だった。



1/144秒毎に更新されているディスプレイとの比較として考えると、1/60秒毎にしか更新されておらず、画面の残像感が1フレーム分残る液晶パネルであることを考慮すれば、妥当な挙動だ。32.2msはデジタルカメラの撮影機能が60FPSであることから想定した最悪値だ。実際の遅延はこれより少ないだろう。

ゲーミングディスプレイの宣伝文句は本当か?


ゲーミングディスプレイの宣伝文句として「フレームレートが高いほど、相手がより早く見える」といった表現がされていたり、それをイメージさせるデモ映像がある。

今回の比較を見る限り、あの宣伝文句はかなり大げさな表現だが、一般的な 60Hz ディスプレイと比べれば確かに10~20ミリ秒ほど早く画面上に敵が映り込んだり、より新しい位置に敵が見えたりするのは事実だろう。

とはいえ、10ミリ秒を切ってくると現在のインターネットにおけるピア間の遅延のほうが大きくなりうるし、それをごまかすための補間や予測などもされているわけなので、まあ、そうね、というレベル感で捉えている。

実際のゲームだとどうなる?


同じように Apex Legends でダミー撃ちする映像をフレーム単位で見てみたが、複雑なApex Legendsの画面は144Hzを60FPSで録画しても、複数フレームが同時に映りこんで逆に破綻した画像になってしまった。体感と異なり144Hzの画像のほうが見づらく、紹介に紹介するような内容にならなかったので、ここでは省略する。

感覚的には、マウス操作による視線移動が発生した場合にダミーや周りの景色がはっきり見えることにより、プレイしやすくなると感じた。

ゲーミングディスプレイはゲーミングディスプレイだった


今回、はじめて60Hzを越えるリフレッシュレートに対応した液晶ディスプレイを買ってみたが、ゲーム画面が見やすくなるなどのメリットが確認できた。特に、 FPS のように視点移動によって画面の表示が大きく変化するようなゲームでは、残像感が大幅に減少するため、疲れにくくなりそうだ。

まだ「ゲーミングディスプレイにするだけでキル数が伸びる」かどうかはわかっていないが、ぼちぼち実践で試してみようと思う。

2021年1月16日土曜日

コロナ禍でやっと自宅ごはんが定着してきたアラフォーのキッチン環境の話

自炊なんてする気はまったくなかったのに、コロナウイルスのせいで日々の95%の食事が自炊になってしまった。まあ、自炊と言えるほどのしっかりした自炊でもないので、敢えて自宅ごはんと書くことにしよう。

コロナウイルスが話題になりはじめてからの直近1年弱で、購入してよかったと思えるキッチン用品などを挙げてみる。


冷蔵庫: SHARP SJ-GW41F

シャープ SHARP プラズマクラスター 冷蔵庫 どっちもドア(両開き・ガラスタイプ) 幅60.0cm スリムタイプ 412L 5ドア ホワイト SJ-GW41F-W

最近まで、2004年の就職のタイミングで購入した130L台の小さな冷蔵庫を使っていたが、日常的に自宅で食事を準備しようとすると、容量的にはかなり不満を感じるサイズだった。また、1-2年前から変なリレー音が聞こえるようになっていたので、冷蔵庫を買い換えたいと思っていた。

ペットボトル等の飲料類がたくさん冷やせることを求めると観音開きは自分に向いていない気がして、また転居の際にもドアの方向がネックにならないようにと考えてシャープの両開きドアにした。別にもっと大きいサイズの冷蔵庫でもいいかなと思ったけど、412Lであれば当面は不自由ないだろうし、横幅600mmであれば転居の際に冷蔵庫の設置場所が制約事項になることはないだろう、という判断でこのサイズに決めた。


しばらく使ってみて感じるのは、もっと冷凍スペースが大きくてもいいかな、という事。冷蔵庫が割とスカスカで、冷凍庫の利用率が非常に高い(後述するエアオーブンが非常に便利だからだ。冷蔵庫とエアオーブンの相乗効果とも言えるかもしれない)。


水切りラック: ヨシカワ 1306081

ヨシカワ 日本製 水切りラック シンクサイド 幅の広がる 2段水切り 15~27.5×57cm 1306081

キッチンの左側スペースに余裕があったので、この奥行きにあわせた水切りラックを購入した。二階建てで、自炊初心者には贅沢すぎるようなサイズである。

後に包丁ホルダーも購入した(同社製品もあるが、私は他社製品を組み合わせている)。これをつけて以降、メインの包丁の定位置はココになった。外れて危ないのではないかと言われることがあるが、インシュロックで固定してあるので、震度7の地震でラック全体がひっくり返る状況でもなければ包丁が飛び出すことはない。


食器棚を持っていないためシステムキッチンの収納スペースしか食器や調理器具の格納場所がなく不便なので、褒められた状況ではないが、水切りラックにモノが残りがち。その状況で、このサイズ感はとても便利に使っている。

この水切りラックはいまの賃貸物件のキッチンの作りにぴったりな仕様のものにしてしまった。日本メーカー製で比較的高級ナモノだけど、将来引っ越しをしたときには、この水切りラックがうまく使えないかもしれない。でもいまのキッチンでの日々が幸せだから、それはそれでいい。


炊飯用の鍋: ストウブ ココットでゴハン S

staub ストウブ 「 ラ ココット de GOHAN ブラック S 12cm 」 ご飯鍋 炊飯 1合 鋳物 ホーロー 鍋 炊飯器 【日本正規販売品】 La Cocotte de GOHAN 40509-653

何年か前に、購入から15年ほど経過した炊飯器の蓋部分が壊れてしまい、それ以後自宅で炊飯できなくなっていた。新しい炊飯器はコレを買う1年前ぐらいから探しはじめていて、ふるさと納税で入手しようかとか、折角だから美味しく炊ける炊飯器がいいなとか色々と考えて市中のラインナップを見ていたのだが、特に美味しそうに炊飯できそうなのはファミリー向けサイズで、私のような独身にちょうど良さそうなサイズのものはあまり選択肢がないと感じていた。

このホーロー鍋に水 200ml と米 1合をいれてしばらく時間をおいた後、ガスコンロ強火で一気に沸騰させて、沸騰したら一度全体をかき混ぜ、蓋をして弱火に。5分後に火を止めたら10分ほど蒸らし、食べられる。炊飯にかかる手間は、強火の間の数分間と、キッチンタイマーからの割り込みが発生した瞬間だけ。平行で洗い物などをしているとこの時間はロスには感じないし、炊飯器での炊き上がりまで数十分待っていた頃がバカらしくなる手軽さである。

米はゆめぴりかの無洗米を使っているので、といではいない。とぐ手間が発生すると自分的にはかなりハードルが上がるかなと思う。毎回1合で炊いているが、大抵は0.5合ぐらいでよかったりもするので、半分はジップロック容器に移して冷凍している。冷凍された米は容器から出してジップロック袋に移し、過去に炊いたが食べていない米はストックしている。本当にすぐに食事にありつきたい時などは、このストックが活躍している。

このホーロー鍋は底の径がかなり小さいので、標準的なガスコンロでは標準の五徳に乗らない。このため、小さい鍋などが載せられる五徳を併用している。

パール金属 五徳 ブラック 外径14cm 鉄鋳物製ミニ ホーロー加工 フェール HB-4198

実はいま使っているのはふたつ目。洗い物を終えた後にガスコンロで加熱して乾かしたりしていたのだが、ひとつ目の個体を加熱しすぎて底を割ってしまったので、二つ目を購入した。空焚きしてはいけないのだが、温度センサーつきのガスコンロだから大丈夫だろうと油断していたら破損してしまった。まあ底が割れていても使っていいみたいだけども、表面のガラス質が欠けていてカケラを食べるるのは気持ち的に嫌だし、錆びやすくもなっており扱いが面倒で、買い直した。


包丁: 京セラ セラミックナイフ

京セラ 包丁 ファイン セラミック 三徳包丁 16cm グリーン 漂白 除菌 対応 無料研ぎ直し券付 Kyocera FKR-160GR

これまで2000円程度の包丁を使っていて、研いでみたりもしたけども、やはり慣れていない人間が研いでもなかなか切れるようにはならなかった。このセラミックナイフは今のところ非常に快適に利用できている。タマネギを切っていても目が痛くならないだけでも大変助かっている。固い食材に対してだと刃が欠けたり、捻ると割れたりというリスクはあると思うが、そういう事はしないように注意している(場合によっては古い包丁を併用)。


エアーオーブン: レコルト Air Oven エアーオーブン ノンフライヤー RAO-1 

レコルト Air Oven エアーオーブン ノンフライヤー RAO-1 レッド

妹の家で、フィリップス製のノンフライヤーでフライドポテトをさくさくっと作っているのを見て、ノンオイルフライヤーは便利で良さそうだなと思っていた。ただ、フィリップス製のノンオイルフライヤーはもう廃番になっており、中古品しか市中では見かけないので、代わりになるものを探していた。グリルやオーブンレンジの代替として利用しているが、かなり手軽に使えるので気に入っている。購入してからの利用頻度は当初を想像を大幅に超えて高い。

このモデルは、ノンオイルフライヤーというよりは、製品タイトルにあるとおりエアオーブンだ。中に電熱器とファンが入っていて、稼働中に空気をかき混ぜる仕組みになっている。フィリップスの本物なノンフライヤーは掃除機並の音を出していた(それぐらい空気を高速に循環させている)が、この製品はそこまでのものではない。

本物のエアフライヤーと比べれば格段に静かなので集合住宅で真夜中に使っても迷惑をかけない点はメリットだが、ノンフライヤーとしてはパワー不足。空気をかき混ぜてくれて、しみ出した油分がカゴの底に貯まるオーブンだと捉えたほうがいい。他の製品と比べると角張っているので、洗い物はしやすい方で、それが頻繁に利用する上で心理的障壁を下げているように感じる(その代わり空気の循環がトレードオフになっているかもしれない)。

電子レンジのオーブン機能で冷凍フライドポテトを加熱するのが地味に手間だが、これを使うととても簡単だ。魚の切り身を焼くのにも使える。コンロのグリルを使わなくてもいいので掃除する場所も減る(私は、もうグリルを使うことはないと思って、排気口を封印してしまった)。冷凍唐揚げは、一度油で揚げられたモノはうまくいくが、揚げる手前で冷凍された唐揚げには対応できないと思ったほうがいい。ノンフライヤーとしての機能を求めるなら、たぶん他にもっといいモデルがあるだろう(買い換えてもいいかもしれない)。


PVCキッチンマット

キッチンマット クリア PVC 45×180cm 厚さ1.5mm クリアマット 台所マット 透明マット ソフト 撥水 おしゃれ 汚れ防止 お手入れ簡単 床暖房対応 滑り止め (180*45cm)

今回のキッチンでは、PVCの透明なキッチンマットを使用している。液体をこぼしても、モップで拭き取るだけで済む。物件の床を汚さないで済むし、洗濯機で洗わなくても簡単に掃除できるのがとても便利。今後も同じようなものを使い続けるだろう。


これを言ってしまうと身も蓋もないシリーズ1: 広いキッチン

最後に、装備というよりは環境的要素について。フルサイズのキッチンは本当に便利だ。自炊慣れしていないシロートの私のような存在にとって、横幅1メートルにコンロと流し台が集約されているようなユニットキッチンでの料理は、経験がない上でハンデを背負わされているようなものだ。広いキッチンがあるだけで、自炊の難易度はガクッと下がる。


これを言ってしまうと身も蓋もないシリーズ2: グロサリー店へのアクセス

2020年までは坂が多い地域に住んでいて、スーパーで食材を購入して自宅に持ち帰るためには必ず坂道を登らないといけなかった。これはかなりのハードルだった。首都圏で車は持っていないので徒歩がメイン、坂が多いこともあり自転車も持っていなかった。

いまは複数のグロサリー店へ徒歩5分以内にアクセスできる立地になったことで、食材を買いにいく行為に対する障壁がとても下がっている。隣のブロックのコンビニに行くレベルの感覚で、ただ税別100円のカットねぎだけを買うつもりで外出するのすら苦ではないレベルである。次に引っ越しするときも、スーパーなどが近い場所にしようと思う。


まとめ: 環境が揃ったら自宅ごはんが楽しくなった 

私はもともと、食事を準備するために時間をかけたくないと思っている。引っ越し前は、起きたらすき家に行って朝食メニューを頼むような生活をしていた。コロナウイルスが問題にならなかったら、こんなに普段から自宅でごはんを食べていなかったか、弁当などに依存していたと思う。1年前の食生活と比べて、1日あたりのコストはコンサバに見積もっても半額以下になっていて、冷蔵庫以外はすでに投資対効果は得られている。

使いやすいツールを揃えるということは、台所に対する苦手意識を和らげてくれて、場合によっては手間や苦痛を取り除いてくれるんだなあと改めて感じている。スノーボードでも下着やフリースなどをユニクロから山用に変更したときに、それまでの苦労から解放されてとても快適になった。自宅でごはんを食べるのも、同じようなものだなあと思う。

この一年間の食生活の変化は、自分の残りの人生の食習慣そのものを変えることになることを確信している。