Procedural Programming w/ C# -アルゴリズムの威力

探索問題

配列 val[] に入っている 2 値の差がある定数 target0 に一致するか、という問題に直面し、まずは愚直にこんなコードを書いてみた。

// C# code

long[] val     = new long[n]; // val[0 .. n - 1] には単調増加な正値が入っている
long   target0 = ...;         // ある正値

for (int i = 0; i < n - 1; ++ i) {
  for (int j = i + 1; j < n; ++ j) {
    if (val[j] - val[i] == target0)
      return true;
  }
}

これが非常に遅い。速くできないかと思い、よく見てみると、val[i]target0j に依存しないことから

for (int i = 0; i < n - 1; ++ i) {
  for (int j = i + 1; j < n; ++ j) {
    if (val[j] == target0 + val[i])
      return true;
  }
}

と変形して、さらに

for (int i = 0; i < n - 1; ++ i) {
  var target1 = target0 + val[i];

  for (int j = i + 1; j < n; ++ j) {
    if (val[j] == target1)
      return true;
  }
}

val[i]target0for (j) の外に追い出してみる。これで定数倍早くしてみた ... と言っても参照と加算を  \frac{(n - 1) \cdot (n - 2)}{2} \rightarrow n - 2 に減らしただけであるため、さほど速くはならない。

しかし、変形してみると for (j)ただの線形探索をしているだけということに気付く。val[] は単調増加 (ソート済み) なので ... 二分探索が使える!

for (int i = 0; i < n - 1; ++ i) {
  var target1 = target0 + val[i];

  if (Array.BinarySearch(val, target1) >= 0)
    return true;
}

for (j) を書き直してついでに

for (int i = 0; i < n; ++ i)
  if (Array.BinarySearch(val, target0 + val[i]) >= 0)
    return true;

と (添字範囲の意訳を含みつつ) 変形してみる。探索範囲が i0 ~ n - 2 から 0 ~ n - 1 に、ji + 1 ~ n - 1 から 0 ~ n - 1 に広がっているにもかかわらず、爆速になった!

計算量を  O(N^2) \rightarrow O(N \cdot log N) と減らせるアルゴリズムを採用したことによる威力。。。たまには LINQ から離れてプリミティブな手続型で書くのも楽しい。

C# LINQ ~ 性能評価 (1) : ラムダ式とローカル関数

きっかけ

いままで 10 年ほど C# を愛用してきて C# でコードを組むときは可読性や開発効率などアプリ層観点でしかロジックを眺めてこなかったが、競技プログラミングC# が使えるのだろうかという疑問をふと抱いたため、30 年ぶりに C++ も書いてみたりして比較衡量し C# をよりプリミティブな面から研究してみた。いろいろと考察したところ、C#C# 7.0 あたり以降で拡張導入された機能を活かせば戦えるのではないかと。

C# で性能を追求するテクニックはいくつかあるが、今回はその中でラムダ式 (C# 3 以降) とローカル関数 (C# 7.0 以降) の性能比較をしてみたい。

結論

いきなり結論。クロージャローカル関数 (local closure) がベスト、静的ラムダ式 (lambda static) がセカンドベスト、クロージャラムダ式 (lambda closure) がワースト。両端の幅は 10% ~ 20% くらい。驚くのは local CLOSURE がベストであるということ。LINQ の術語によく使う lambda static はまあまあ優秀だが絶対王者ではない。(<< は揺らがない順序関係、< は環境などによっては揺らぐ可能性がある順序関係。)

(inferior) lambda closure << local static << lambda static < local closure (superior)

実用としては、保守性・可読性の観点から通常通り静的なラムダ式で書くことでよいが、外部変数を取り込むときはラムダ式の使用は禁止にしてローカル関数で定義するのが望ましい、と考えられる。

理由

なぜこうなるのか。closure が static より速いとも言えないし、local が lambda より速いとも言えない。一見まちまちに見えるこの結果にも理由があり、いろいろなサイト *1*2 で解説されているところを考察すると見えてくるものがある。

背景:ラムダ式は匿名オブジェクトの一種であり、匿名型と同様にコンパイラがオブジェクトを生成し、「(名前管理空間としての) クラス 」に属するように構成される

ラムダ式は記号のように見えるし、プログラマにとってはソースコードの地の文に置かれる関数と取り扱い方が異なるが、コンパイラVM にとっては一般のクラス・メソッドと同じく「オブジェクト」である。関数は機能 (function) という抽象概念に見えるが、ノイマン型コンピュータではメモリに載るオブジェクト (= データセット) であり、.NET においてはクラスを単位として管理されるオブジェクトである。したがって、すべてのメソッドはクラスに紐づけられる。というか、C/C++ とは違い、 C# ではそもそもクラスから独立して単独で関数定義することができない。

理由1:.NET においては、静的クラス・静的メソッドよりもインスタンスクラス・インスタンスメソッドを重視して最適化されている

C# / .NETオブジェクト指向言語 / フレームワークであるため、定義することの少ない静的クラス・静的メソッドよりもインスタンスクラス・インスタンスメソッド寄りに最適化するのは当然である。

そしてそのために、静的メソッドよりもインスタンスメソッドの方が実行効率が高い、という現象が起きる。逆に言うと、はやりの関数型プログラミングからすれば望ましい、内部状態 (インスタンス) を管理する必要のない参照透過的な「静的メソッド」であっても、なんと、(インスタンスメンバー変数を参照するなどして) 敢えてインスタンスメソッドにしてしまう方が .NET では実行効率が向上する!

これが下記になる理由。

local static << local closure
lambda static < local closure
理由2:ラムダ式はオブジェクト化されるが、クラスオブジェクトと同等の最適化は期待できない

ラムダ式はクラスから独立しているようにプログラマからは見えるが、VM 内ではクラスに紐づいて管理されている。ということは、実行時にオブジェクト化のコストがかかることになる。オブジェクト化自体のコストはインスタンスクラス・静的クラスも同じだが、匿名オブジェクト (ラムダ式・匿名関数・匿名型) はクラスオブジェクトと同等の最適化がなされない、という点がもう1つのポイントになる。

プログラマからは同一に見える匿名オブジェクトでもコンパイラ / VM にとっては同一か否か判別できない。例えば、匿名型インスタンス new { Index = 1, Value = 1 } と別に記述したもう1つの new { Index = 1, Value = 1 } はプロパティの並び順が同じであるためコンパイラは同じ「型・クラス」と認識してくれるが、同じ値であっても VM は同じ「インスタンス」とまでは認識してくれない。それと同じようにラムダ式も同じオブジェクトとは認識してくれないケースがある。それがクロージャだ。

クロージャは、自身が参照するすべての変数を管理しなければならない。内部変数は通常の関数におけるローカル変数に相当するが、外部変数は外部変数で管理が必要である。ラムダ式にとっての状態遷移しうる外部変数の位置づけはインスタンスメソッドにおけるメンバー変数と同等であるため、プログラマクロージャラムダ式を定義するとコンパイラは外部変数をメンバー変数とする匿名クラスを定義する。このとき外部変数はヒープに確保される。

さて、同一の外部変数 int y をキャプチャーする同一記述のクロージャラムダ式 (int x) => x + y が複数あったとして、それらが同じクラスの同じメンバーメソッドになるであろうか? プログラマの視点では、理屈上、同一であると認識できる。が、コンパイラにとっては構文解析を超高度化しないと実際にはムリであろう。というわけで、同一記述のクロージャラムダ式であっても VM は同一とは認識せず、別個にクラス・オブジェクト化すると思われる。ということは、同一に見えても別クラスとなり、したがって記述箇所の異なるラムダ式は同一記述であっても別の関数オブジェクトが利用され、同一のコントロールフローが利用される頻度も低下する。結果として、キャッシュ効率も悪くなり、コード最適化の効果も薄いため最適化がかからない、ということになる。クラスメソッドやローカル関数で int add(int x) => x + this.y; と書いた場合は、VM は関数ポインタでメソッドを、メンバー変数で this.y を一意に特定できる。したがって、こちらは多頻度利用され、最適化もされると期待できる。

これが下記になる理由。

lambda closure << local static
lambda closure << lambda static

計測プログラム

  10^6 LINQ で計算する処理を  10^3 回 for イテレーションして計測する。

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;

public class StopWatch : Stopwatch {
  public void DispElapsed(string text) {
    base.Stop();
    Console.WriteLine($"{text} : {base.ElapsedMilliseconds} ms");
    base.Reset();
    base.Start();    
  }
  
  [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
  public static void Main() {
    var sw = new StopWatch();
    int y  = 3;

    int func1(int x) => x / 7 + 3;  // static  local function
    int func2(int x) => x / 7 + y;  // closure local function

    sw.Start();

    for (int i = 0; i < 1000; ++ i)
      Enumerable.Range(0, 1000000).Select( x => x / 7 + 3 ).Count(); // static lambda

    sw.DispElapsed("lambda static ");

    for (int i = 0; i < 1000; ++ i)
      Enumerable.Range(0, 1000000).Select( func1 ).Count();

    sw.DispElapsed("local  static ");

    for (int i = 0; i < 1000; ++ i)
      Enumerable.Range(0, 1000000).Select( x => x / 7 + y ).Count(); // closure lambda

    sw.DispElapsed("lambda closure");

    for (int i = 0; i < 1000; ++ i)
      Enumerable.Range(0, 1000000).Select( func2 ).Count();

    sw.DispElapsed("local  closure");
  }
}

計測した結果、下記のような結果に。何度か計測しても同じ環境上ならほぼ同じ結果になる。ハードウェアは同一の i9-10900 + RAM 32GB。

[.NET Core 3.1 / C# 8.0] Windows 10 Pro
lambda static  : 2991 ms
local  static  : 3032 ms
lambda closure : 3088 ms
local  closure : 2581 ms
[.NET5 / C# 9.0] Windows 10 Pro + WSL2 + Ubuntu20.04LTS + Docker + .NET Interactive + Jupyter Lab
lambda static  : 2742 ms
local  static  : 2758 ms
lambda closure : 2956 ms
local  closure : 2760 ms

おわりに

LINQ は、言語間比較の際に誤った評価手法で「遅い」と結論付けられ、不当な扱いをされることの多いかわいそうな子。

LINQ の遅延評価を即時評価に切り替えるために .ToArray().ToList() でオブジェクト実体化しているケースがあるが、その評価手法は間違っていることが多い。

.ToArray().ToList() もどちらもヒープにメモリ確保してそこにデータをコピーするが、その性能が LINQ が実行したい本質 (今回のラムダ式やローカル関数) に比べて圧倒的に悪いため、実体化自体を計測したいのでなければ意味をなさない。

.Count() であれば、全量をシーケンシャルに操作しつつも、余分に消費する変数が int (count) 1つ分であるため、(計算結果は保持されないが) LINQ 自体の計算速度の評価としては適切となる。

4.0 GHz CPU の場合、 10^9 回まわして約 3,000 ms というのは1回あたり 12 Clock Cycle に相当する。整数除算 x 1 + 整数加算 x 1で 12 Clock Cycle は非常に妥当で、計算、イテレーション、関数呼び出しのコストは、いずれもまったく悪くない。LINQ が遅くなるのはオブジェクト実体化のやり方が誤っているか下手なのだと思う。

LINQ を用いてオブジェクト実体化 (計算結果格納) を伴う実コードの性能評価をする際は、オブジェクト実体化を1回のみ、効率的に実行する方法を含めて検討しなければならない。

ただ C# は値型配列であってもヒープにメモリ確保するため、.ToArray() を回避して簡単・簡潔に stackalloc[] にする方法が欲しい。stackalloc[] が関数を超えられないため仕方ないのだが、stackalloc[] に書き込むためだけに

Span<int> sa = stackalloc int[n];

foreach (var (result, i) in seq.Select( (x, i) => (func(x), i) )) {
  sa[i] = result;
}

という LINQ (関数型) と foreach (手続型) が混在するコードを書くのもね ... あるいはこうか。わざわざ unsafe 持ち出しても、ジェネリックでデータ型を記述できなくなる上に記述量が増えているからボツかな。。。

public class Test {
  public static void Main() {
    var       seq  = Enumerable.Range(0, n).Select( x => func(x) );
    Span<int> span; unsafe { int* sa = stackalloc int[n]; span = new Span<int>(seq.Write(sa), n); }
  }
}

public static class Extension {
  [MethodImpl(AggressiveInlining)]
  unsafe public static int* Write(this IEnumerable<int> src, int* dst) {
    foreach (var (x, i) in src.Select( (x, i) => (x, i) ))
      dst[i] = x;

    return dst;
  }
}

環境構築メモ ~ Visual Studio Code + WSL2 + Ubuntu + Docker のコンパイル環境構築時の注意

目的

VS Code での C# コンパイル環境を構築するにあたり、Build は Windows から、Run は Docker コンテナから実行するようにしたい。

C#C++ を混在開発するにあたり、C# の Build までの作業のみ Windows で、それ以外の作業はすべて Docker コンテナで実施したい。初期条件を様々に変更して Run するテスト環境や C++ 開発環境の構築・整備は OS が汚れる可能性があるため、Windows でやりたくない、というのが理由。

概要手順

Docker コンテナから、コマンド1つ叩くと (複数の) C# アプリコンソール開発プロジェクトが整備される環境を構築する。

起動すると複数のフォルダを作成し、1つ1つにテストデータを格納していく Linux ツールがある。それを起点として dotnet コマンドを実行し、C# 開発プロジェクトを作成していく。

Docker コンテナの一般ユーザホームに下記のシェルスクリプトを記述・作成し、コンテナ起動時に実行されるようにする。

function func_dotnet() {
  # メインフォルダの下に各プロジェクトごとのテストデータを配置していくツール (仮に $COMMAND とする)
  ${COMMAND} $1
  cd $1

  for DIR in `find -maxdepth 1 -mindepth 1 -type d`
  do
    cd $DIR
      # .NET Core 3.1 を指定して C# Console App を新規作成
      dotnet new globaljson --sdk-version 3.1.413
      dotnet new console --force

      # Program.cs と .csproj は、デフォルト版を破棄し、事前準備したテンプレート版に差し替え
      rm Program.cs
      rm $DIR.csproj
      cp /cs/Template/* .
      mv ./Program.csproj ./$DIR.csproj

      # NuGet パッケージインストール :【注意点後述】 Windows 環境から Build する場合ここで add package してはいけない (ものもある)
#     dotnet add package ...
    cd ..
  done
}

alias newproj=func_newproj

注意点

フォルダ名に C# C++ などの記号を入れてはならない

記号を入れた場合、OS レベルでは正しく動くが、VS Code が記号を認識できずにデバッガが誤動作する。tasks.json に記述されたデバッガ "problemMatcher": "$msCompile" がフォルダ C# を C までしか認識できないため、エラー行へのクリックジャンプができず、また、うっとおしい警告ポップアップが出るようになる。

Windows / Linux 双方から同一のものを参照するように NuGet パッケージ格納先を変更・共通化する

Windows/Linux 共通で使えるコマンド dotnet add package は、パッケージを格納し、そのパスを .csproj や obj フォルダ内の json に書きこむ。

格納先フォルダが環境により異なるため、そのままでは、build を実行する環境以外でパッケージインストールするとモジュールが見つからなくなる。

  • Windows では %UserProfile%/.nuget/packages
  • Linux では ~/.nuget/packages

そこでまず、%UserProfile%\AppData\Roaming\NuGet\NuGet.Config ファイルに下記の記述を追記し、NuGet パッケージ格納先フォルダを Linux から見える位置に移動する。さらにそのフォルダを Docker コンテナにマウントすることで、Windows / Linux 双方が同一の NuGet パッケージ格納フォルダを参照するようにする。

<configuration>
  ...

  <config>
    <add key="globalPackagesFolder" value="c:\ ...(new folder) ... \.nuget\packages" />
  </config>
</configuration>

それでもまだ注意が必要なものがあり、パッケージ (dll) の中から再帰的に他のパッケージを (動的に?) 呼び出しているものは、build 環境と異なる環境から dotnet add package すると build 時に参照が解決できないと怒られる。

*.csproj の * 部分は格納フォルダ名と同一にする

同一にしない場合、NuGet が obj フォルダ内の json を複数作成したり、取り違えたりするリスクがある。

各プロジェクトへの独自ライブラリ展開は、ソース (.cs) かバイナリ (.dll) か配布方法が異なる

ソース配布の場合はソースフォルダをコピーするだけでよいが、バイナリ配布の場合はソースフォルダをコピーしてはならない。バイナリ作成時の .csproj がプログラム作成用フォルダに展開されるため、コンパイル用の設定が競合する。

バイナリとして配布する場合は、バイナリ作成時に用いた .csproj への参照をプログラム作成用 .csproj を記述してやるだけでよい。

  <ItemGroup>
    <ProjectReference Include="..\..\UserLib\UserLib.csproj" />
  </ItemGroup>
*.csproj に NuGet インストールされるパッケージの参照をあらかじめ書き込まない

事前に手動で書き込んでしまうと、何が正しくインストール・参照設定されたものであるかがわからなくなる。

今回分かったこと

.NET はクロスプラットフォームで便利なものではあるが、実はバイナリ互換性は一部保証されていない。小さな自己完結プログラムであればバイナリ互換であるが、dll 参照などはたとえ dll が揃っていても、環境が異なるとワークしないことがある。

build 環境で Program.exe を走らせると問題ないのに、同じコードから生成したバイナリであっても 、dotnet publish で build したり、実行時動的結合しているであろう dotnet run で走らせると参照エラーになったりする。
.NET Core 2.x までは ILMerge でライブラリを静的結合して exe を生成できたが、.NETCore 3.0 以降は ILMerge は使えず、単一実行ファイル生成するには代わりに .csproj ファイルで Publish 指定する。このときに指定すべき項目を見るとはっきりわかるが、win-x64 や linux などとアーキテクチャを指定しなければならなくなっている。

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
    <PublishSingleFile>true</PublishSingleFile>
    <SelfContained>true</SelfContained>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <PublishReadyToRun>true</PublishReadyToRun>
  </PropertyGroup>

.NET Framework .NET Core → .NET5 → .NET6 と進むにつれ、クロスプラットフォーム化で選択可能なアーキテクチャの幅が広がりつつあるのは望ましいことであるが、それはソースコード互換を意味しており、バイナリ互換で動作することを期待してはいけない*1、ということのようだ。VM なのにちょっと不思議。

*1:異なる環境へのバイナリ・インポートは厳禁であり、リビルドが必要。

環境構築メモ ~ Visual Studio Code + WSL2 + Ubuntu + Docker + LaTeX のコンパイル環境

目的

LaTeX コンパイル環境を構築したい。LaTeX はモジュールや文字フォント等で複雑な依存関係がありそう、ということで、構築失敗による環境汚染を避けるために Ubuntu Docker コンテナで実現したい。LaTeX の文書作成 & PDF 閲覧は Windows 上の Visual Studio Code で行いたい。(あれもこれも、欲張りだなぁ ...)

前提

Windows 10 上には Visual Studio Code と SumatraPDF *1 が、WSL2 上には Ubuntu 20.04 LTS と Docker が既に導入されているものとする。

成果

以下の手順を踏むと Visual Studio Code にて次のことが可能になる。

  • Ctrl + Alt + B (Build) でビルド
  • Ctrl + Alt + V (View PDF) で PDF 閲覧
  • Ctrl + Alt + C (Clear intermediate files) で中間ファイル削除

一度、ビルドが通ると、編集 → ファイル保存、するだけで自動リビルドされる。

TeX 記述サポートにおいても、@a と書くだけで \alpha に補完される、など Visual Studio Code はかなり便利。LaTeX Workshop のスニペットこちら

Ubuntu での LaTeX 実行環境構築

こちらの記事 によると Docker で日本語 LaTeX をやるには paperist/alpine-texlive-ja イメージで uplatex コマンドを使うのがよいらしい。ということで、このイメージを Docker Hub から docker pull しておく。次に ~/.bashrc に以下のように記述しておく。

# latex through docker
function func_tex2pdf() {
  docker run --rm -it -v /mnt/d/[working folder path on raw Ubuntu]/$1:/workdir paperist/alpine-texlive-ja latexmk -pdfdvi -latex=uplatex -e '$dvipdf=q/dvipdfmx %O -o %D %S/;' -synctex=1 -silent $1.tex;
}

alias tex2pdf=func_tex2pdf

これで WSL2 Ubuntuコマンドラインから tex2pdf Sample と打つことで、Docker コンテナをワンタイム起動し、コンテナ内の作業フォルダ /workdir にある tex ファイルから同フォルダに pdf が生成できるようになる。ここでのポイントは以下のとおり。

  • コンテナの /workdir-v オプションでホスト Ubuntu の作業フォルダにマウントしている
  • さらに当該 Ubuntu フォルダの実体は Windows 側の NTFS ドライブ (/mnt/d/ すなわち d:/) にある
  • 環境変数 $1 にはコマンドの第1引数 (上述の例では Sample) が入り、共通フォルダの下のサブフォルダ Sample を作業フォルダとしてその中の Sample.tex を対象ファイルとしている
  • 環境変数$1alias 文内で2度使えないため、関数定義にする
  • オプション -synctec は後述 (Ubuntu 環境では利用する予定なし)
  • uplatex や dvipdf による一連の作業を latexmk を用いたワンライナーで実行する (texwiki 参照)
  • 当該イメージ内に dvipdf がないため、代わりに dvipdfmx を用いることを Perl 表記の引数で指示している *2
  • マウント対象パスのスペース文字はエスケープシーケンス \ ("\" + Space) になる

Visual Studio CodeLaTeX 作業環境構築

こちらの記事 に従い、Visual Studio CodeLaTeX Workshop 拡張機能をインストールする。

Visual Studio CodeJSON 設定

次に Power Shell から WSL を通じて上述の Docker イメージのワンタイム起動を行うように Visual Studio Code に設定する。ただし先ほど定義した .bashrc の tex2pdf エイリアスをそのまま利用することはできない。Visual Studio Code の設定 (JSON ファイル *3 ) には以下のように記述する。

    // -- LaTeX Workshop --
        // 使用パッケージのコマンドや環境の補完を有効にする
        "latex-workshop.intellisense.package.enabled": true,

        // 生成ファイルを削除するときに対象とするファイル
        // デフォルト値に "*.synctex.gz" を追加
        "latex-workshop.latex.clean.fileTypes": [
            "*.aux", "*.bbl", "*.blg", "*.idx", "*.ind", "*.lof",
            "*.lot", "*.out", "*.toc", "*.acn", "*.acr", "*.alg",
            "*.glg", "*.glo", "*.gls", "*.ist", "*.fls", "*.log",
            "*.fdb_latexmk", "*.snm", "*.nav", "*.dvi",
            // "*.synctex.gz", // 中間ファイル消去後も SyncTeX 利用の場合はコメントアウト
        ],
    
        // ビルドのレシピ
        "latex-workshop.latex.recipes": [
            {
                "name": "tex2pdf",
                "tools": [
                    "tex2pdf",
                ]
            },
        ],
    
        // ビルドのレシピに使われるパーツ
        "latex-workshop.latex.tools": [
            {
                "name": "tex2pdf",
                "command": "wsl",
                "args": [
                    "docker",
                    "run",
                    "--rm",
                    "-v",
                    "/mnt/d/[working folder path on raw Ubuntu]/%DOCFILE%:/workdir",
                    "paperist/alpine-texlive-ja",
                    "sh",
                    "-c",
                    "latexmk -latex=uplatex -synctex=1 -silent %DOCFILE%.tex && dvipdfmx %DOCFILE%.dvi"
                ],
            },
        ],

        // PDF Viewer の表示方法
        "latex-workshop.view.pdf.viewer": "tab",      // Internal PDF Viewer
     // "latex-workshop.view.pdf.viewer": "external", // External PDF Viewer

        // SyncTeX
        "latex-workshop.view.pdf.internal.synctex.keybinding": "double-click", // or "ctrl-click" 
        "latex-workshop.synctex.afterBuild.enabled": true,
        "latex-workshop.synctex.path": "synctex",
        "latex-workshop.synctex.synctexjs.enabled": true,

        // Internal PDF Viewer Settings
        "latex-workshop.view.pdf.zoom": "page-width",

        // External PDF Viewer
        "latex-workshop.view.pdf.external.viewer.command": "c:/Users/[user name]/AppData/Local/SumatraPDF/SumatraPDF.exe",
        "latex-workshop.view.pdf.external.viewer.args": [
          "-reuse-instance",
          "%PDF%",
        ],
        "latex-workshop.view.pdf.external.synctex.command": "c:/Users/[user name]/AppData/Local/SumatraPDF/SumatraPDF.exe",
        "latex-workshop.view.pdf.external.synctex.args": [
            "-reuse-instance",
            "%PDF%",
            "-forward-search",
            "%TEX%", // Sumatra PDF がなぜかこのパスを正しく認識しないため、順方向検索が無効
            "%LINE%",
            "-inverse-search",
            "\"c:\\Users\\[user name]\\AppData\\Local\\Programs\\Microsoft VS Code\\bin\\code.cmd\" -r -g \"%f:%l\"",
        ],

ここまでの作業により、Visual Studio Code のビルドコマンドを実行すると、WSL と Docker を通じたワンライナーTeX 文書をコンパイルするようになる。ここまでのポイントは以下のとおり。

  • Power Shell から起動する WSLbash は対話シェルではないため、.bashrc が反映されない
  • したがって、Power Shell から WSL コマンドでワンライナー起動する
  • 対話シェルではないため、docker コマンドの引数 -it は付けない
  • JSON 設定の latex-workshop.latex.tools セクションに記述するワンライナー.bashrc と若干違うのは、dvipdf 設定の引数部分である Perl 記述が (どうエスケープシーケンスを書いても) 失敗してうまく起動しないため
  • 環境変数 %DOC% は用いず %DOCFILE% のみを用いるのが吉 (%DOC% の挙動が不安定で何度かハマっている。 参考記事 参照)
  • オプション -synctex によりコンパイル時に .synctex.gz ファイルを生成し、これにより tex と pdf 両ファイルでの項目位置を紐づけ、クリックによる相互ジャンプを可能にする (後述)
  • マウント対象パスのスペース文字はエスケープシーケンス不要になる
PDF Viewer の設定

この記事この記事 を参考に synctex 関連を設定する。

Sumatra PDF を起動し、メニュー > 三 > 設定 > 詳細設定 を開き、設定ファイルの下記部分を修正する *4

EnableTeXEnhancements = true
InverseSearchCmdLine = "c:\Users\[user name]\AppData\Local\Programs\Microsoft VS Code\bin\code.cmd" -r -g "%f:%l"

これにより、Sumatra PDF をダブルクリックしたときに、逆順検索 (ダブルクリック箇所に対応する Visual Studio Code 上の LaTeX ファイル該当箇所へジャンプ) する。

課題

Visual Studio Code 内タブの PDF ビューアも外部 PDF ビューア Sumatra PDF も起動・表示・ビルド時再読み込みはうまく動作するが、いまのところ synctex は外部 PDF ビューアからの逆順検索しか成功しない ...
内部ビューアの場合は、おそらく Visual Studio Code 内の Ctrl 押しながら系の操作がキャンセルかインタラプトされている。
外部ビューアの場合は、Sumatra PDF が -forward-search の次の引数のファイルパスを正しく読み込めていない。不明なソースファイル (C:\ ..... \Sample.tex) と表示される。そのパスにファイルがあるにも関わらず。
継続調査。

*1:PDF をファイルロックせず、更新時に即時再読み込みをしてくれる PDF ビューア。Windows 上で Visual Studio Code + LaTeX Workshop 利用する時の推奨ビューア。

*2:クォーテーションがネストするため、Perl 部分は q// で表現する。

*3:%UserProfile%\AppData\Roaming\Code\User\settings.json

*4:ここでは、環境変数 %UserProfile% は無効。

Visual Studio Code へのエディタ移行

四半世紀の間、使い続けてきた emacs 系エディタから VS Code に移行しようかと思う。

  • 第1の理由 ... Visual Studio の代替
  • 第2の理由 ... コーディング支援 (のシーン限定での利用可能性)
    • JavaScript などたまにしか書かない動的型付け言語は、短いコードであれど、慣れた emacs 系エディタで書くよりも、コード補完やシンタックスハイライトがある VS Code の方が便利で速い、という事実に改めて気づいた。
    • よく書く静的型付け言語 (C#) では、基本 API が頭に入っており、思考やタイピングがコード補完よりも圧倒的に速いため、コーディングの妨げにしかならない支援 (... か?) をする IDE を嫌っていた。が、文脈 (言語) 別にカスタマイズすればよいわけで、シンプル・エディタ至上原理主義を卒業してよいのかもと思うようになった。
  • 第3の理由 ... emacs を代替するエディタ機能
    • emacs 系エディタでよく使う機能が何か棚卸してみたところ、後述する1機能を除けば、以下の通りだった。これらはすべて VS Code でもできる。むしろ 2 - 5 については emacs ではショートカットキーに続けてコマンドをタイプしなければならないところを、VS Code ではショートカットキーだけにできそう。
      • 1. undo (取消) / redo (やり直し)
      • 2. search-forward (逐次検索) / query-replace (逐次置換)
      • 3. copy-rectangle (矩形選択コピー) / yank-rectangle (矩形貼り付け)
      • 4. replace-regexp (正規表現置換)
      • 5. change-buffer-encoding (エンコーディング変換)
      • 6. シェル呼び出し / コンパイル
  • 第4の理由 ... 前方互換性と入手容易性
    • write once, run anywhere というか accustomed once, utilize anywhere の予感がする。VS Code は次世代のデファクトスタンダードになってきたし、OS を選ばず、さらに Windows から WSL に対しても使える。Visual Studio やお気に入りの emacs 系エディタをよその環境にインストールしてくれとリクエストしても却下されがちだが、VS Code はそのハードルが低くなりそう。この点と第3の理由から、emacs キーバインド拡張機能を入れずに、素の VS Code キーアサインに慣れておくのもよいかもと思うまでになった。
    • VS Code の出現でシェアが下がった emacs の利用継続にリスクを感じたことに端を発し、メンテナンスが止まってかなり立つ xyzzy*1 を常用している現状をどうにかする必要があると考えた。
  • 第5の理由 ... 利用サービス/アプリのオフライン・オンライン間統合
    • 第4の理由の延長線上にある欲求として、利用するサービス / アプリがオフライン・オンライン間で分離することを許容せず、統合したい、という思いがある。同じ作業をするときは、ローカルでもクラウドでも、同じサービスを同じ設定で使いたい。VS Code は近いうちに Web サービスにもなりそうであるため、期待している。その背景にあるものは次のとおり。
    • 出張や旅行で海外に出てもコンピュータ環境が脆弱にならないように、長いこと軽量かつ高性能なモバイル PC を使い続けてきた*2 が、最近はスマホタブレットにモバイル PC に ... と高性能 CPU と大容量ストレージをいくつも保持するのが経済的にバカらしいと思うようになってきた*3。そこでここ2年くらいは VPN + RDP を活用する前提で、ハイエンド・デスクトップ PC + ミドルクラス軽量 2 in 1 端末 + ローエンド・スマホ、に収斂させる方向で環境整備しようとしている。2 in 1 端末は軽量・安価・必要以上にパワフルでないことが重要で、テキスト編集、VPN + RDP、スマホ用途 (Web ブラウズ、SNS、メッセ、動画視聴) ができればよい。という状況で、出先でちょっとテキスト編集するときにはオフラインローカルで、本格的に使いたいときには自宅サーバにつなぐかクラウドサービスで、でもスペックの違うそれぞれ環境に固有のサービスではなく、同じものを同じ設定で使うという Chrome Book 的な活用コンセプト*4 をパワフルでない 2 in 1 端末で実現できたら理想だと考えている。そのためには軽量で anywhere 的な (でもいざとなったらヘビーな使い方もできる) エディタが必要になる。
    • 環境整備のもう1つの系譜としてタスク / メモ管理がある。こちらも四半世紀の間、付箋紙アプリ → FitzNote → DropBoxEvernoteOneNoteGoogle Keep と試してきて、最近 Notion *5 に乗り換え / 一本化することにした。同じ作業をするのにローカルとクラウドで道具や操作を泣き別れさせたくないという欲求は強い。


右サイドバーにコード・ナビゲータが表示されるとか、Jupyter と併用するときになんとなく UI が似ている*6 とか、VS Code 移行によるメリットはその他にも多数ある。

一方で、どうしても解消できないデメリットが1つだけあり、それこそが本格移行をいままで渋っていた理由でもある。それは emacs のようなタブストップ&ラインインデント*7 が効かないこと。emacs 以外のエディタは、なぜかタブキーをタブ入力か複数スペース入力にしか設定できない。これは諦めて新スタンダードに合わせる必要があるか ...

*1:Windows 上で動く emacs ライクな軽量テキストエディタ。2014 年 4 月を最後にメンテナンスが止まっている。

*2:Dynabook SS → Libretto 50 → VAIO type-p → VAIO Duo 11 → ... その後、小型モバイル PC は当たりに恵まれず ... をネット黎明期から海外に持ち歩いているモバイラーである。

*3:ひどい時には、会社 PC + 個人 PC + 会社スマホ + 個人スマホ + 個人タブレットで合計 5kg 程度をハンドキャリーすることがあり、重量の観点でもバカらしかった。

*4:Chrome Book はコンセプト的には「軽量、安価、でもオフィスワークできる」だが、実際には WAN 回線がないモデルが大多数だし、野暮ったくて重量があるため、持ち歩くには実用的ではない。B5 サイズ弱、タイプカバー付きで 650g 前後、Geekbench 5 (64bit MultiCore) スコア 2500 ~ 3000、LTE あり、USB Type-C OTG + DP Alternate mode あり、 5 万円程度が理想。現時点でこのコンセプトを最も体現しているのは Chrome OS ではないが Huawei Mate Pad 10 になる。

*5:コードと結果とメモを混在させて記述・記録できる Jupyter のように外部ソースと自分のアイディアを取り混ぜて簡単に記述できるのが Notion の利点。

*6:VS Code と Jupyter の使い分け・棲み分けを考えなければならない。

*7:文脈から決まる、各行におけるインデント位置にカーソルとコード先頭をジャンプさせること。どこにいてもあるべきインデントに整形してくれるため、うっかりスペースを打ったりしてもタブを押せば戻せるし、うっかりタブを打っても何も起こらない。オフサイドルールの Python には必須の機能ではないかと思うのだが、なぜか emacs 以外に実装しているエディタがないように見受けられる。

C# on Jupyter からの Numpy.NET 利用

Jupyter から C# で Numpy.NET ライブラリを利用できないかと思い、

#r "nuget: Numpy, 3.7.1.25"
using Numpy;

と記述するも、結論としては NG。このようなランタイムエラーが出る。

System.DllNotFoundException: Unable to load shared library 'python37' or one of its dependencies. In order to help diagnose loading problems, consider setting the LD_DEBUG environment variable: libpython37: cannot open shared object file: No such file or directory
   at Python.Runtime.Runtime.Py_IsInitialized()
   at Python.Runtime.Runtime.Initialize(Boolean initSigs)
   at Python.Runtime.PythonEngine.Initialize(IEnumerable`1 args, Boolean setSysArgv, Boolean initSigs)
   at Python.Runtime.PythonEngine.Initialize(Boolean setSysArgv, Boolean initSigs)
   at Python.Runtime.PythonEngine.Initialize()
   at Numpy.np.InstallAndImport(Boolean force)
...


環境は、WSL2, Ubuntu 20.04LTS, Docker (tensorflow/tensorlow-latest-py3-jupter) イメージ, Jupyter Lab, Python3.6, .NET Interactive, dotnet-sdk-5.0 (ver. 5.0.302)。エラーを克服すべく試行錯誤すること半日。apt-get による Python 3.7 の追加インストール、.NET SDK 5.0.202 の追加インストール、python37.dll の追加配置、環境変数追加等々、エラーから推測されることを試すもすべてダメ。.NET Interactive ではなく Windows 上の Visual Studioコンパイルすると OK。

調べていくと Numpy は Numpy → Python.Included → Python.Deployment → pythonnet_netstandard_py37_win というパッケージ依存があり、この pythonnet_netstandard_py37_winC# から Python につなぐブリッジなのだが、その解説には

Python and CLR (.NET and Mono) cross-platform language interop. Compiled against .NetStandard 2.0 and CPython 3.7 (win64)

とある! Python and CLR (.NET and Mono) cross-platform language interop なのに、繋ぐ先として CPython 3.7 (win64) を想定してコンパイルしてあるだと!

クロスプラットフォーム .NET 5 のおかげで Windows でも Ubuntu でも PythonC# が (ビルド後 dll でもあっても) 使える write once, run anywhere 状態なのに、ブリッジ先が win64Python パッケージであるため、Jupyter (= Ubuntu) 上で記述した C# から接続できない … それは cross-language ではあっても cross-platform ではないのではないか ...


代わりに NumSharp ライブラリは使える。

#r "nuget: NumSharp, 0.30.0"
using NumSharp;

var x = ((NDArray) new double[] { 1, 2, 3, 4 }).reshape(2, 2);
var y = ((NDArray) new double[] { 4, 3, 2, 1 }).reshape(2, 2);

Console.WriteLine(x.dot(y).ToString()); // 行列の積
Console.WriteLine((x * y).ToString());  // アダマール積

FizzBuzz

学生時代に情報科学科の同級生たちと「FizzBuzz 問題というものがあるらしいが、この程度の基礎ができないプログラマなどいるはずがない」と会話していた。

それから四半世紀。小学生にはプログラミングの授業があり、非 IT 企業の一般社員にも「Python 書いて RPA しろ、HTML/CSS/JavaScript で社内サイト作って情報発信しろ」という圧が掛かってくる DX 社会。DX とはデジタル革命, IT, Web2.0, ICT に続く "企業経営者向け IT 売り込みバズワード" だと思っていたが、一般の人々にまでやれ!やれ!と煽り立てる世の中の圧が今回はすごい。

猫も杓子もコードを書けという時代になった *1 せいか、Quora などでも FizzBuzz 書けない問題をよく目にする。

ということで、自分なら FizzBuzz どう書くか、C#Perl でやってみた。

C# 9.0 (標準解)
Enumerable.Range(1, 30)
  .Select( i => (i % 3, i % 5) switch {
    (0, 0) => "FizzBuzz",
    (0, _) => "Fizz"    ,
    (_, 0) => "Buzz"    ,
    (_, _) => "" + i    ,
  } )
  .ToList().ForEach( x => Console.WriteLine(x) );

所要 1 分半。using, namespace, class, Main() メソッドの宣言が不要な C# スクリプトとして記述した。

普段使いの C# のため、コアロジックのコーディングに悩むこともリファレンスを参照することもなく、呼吸するようにタイピングする。純粋関数 (LINQ によるデータ変換) と I/O (WriteLine() による出力) をきれいに分離した上で後者をどう書くかというのが C# LINQ を使う上で最も時間を費やす悩みどころ。結局、WriteLine() を交えるなら List の .ForEach() を使うのが (開発効率的に) 早いだろうということで、メモリ効率を犠牲にしてこの形に。

数列のサイズが大きいか未知である場合は、メモリ効率を勘案し "データ変換 + 出力" を単位として遅延評価にする。その場合、最後を foreach {} 制御に置き換えるところなのだろうが、カーソル移動とブロックインデントが面倒なため、LINQ メソッドチェーンで次のように書いてしまう。ステートメント混じりである点とダミーデータを作って捨てるムダが美しくないが、デバッグなどではよく使う。

  .Select( x => { Console.WriteLine(x); return 1; } ).ToList();
Perl 5
map do {
  my $s = ($_ % 3 ? '' : 'Fizz') . ($_ % 5 ? '' : 'Buzz');

  print $s ? $s : $_, "\n";
}, (1 .. 30);

所要 3 分。同一内容の記述であるが、こちらの方がコーディングの所要時間が長くなった。

Perl は得意ではあるもののたまにしか使わないため、いかに簡便に書けるか条件判定と演算子との組み合わせ *2 を改めて意識する必要があり、また、map の対象をステートメントブロックにするか式にして手続を外に追い出すか試行錯誤したため、そこに時間を取られる。

Perl 5 (別解)
print map{(($s=($_%3?'':'Fizz').($_%5?'':'Buzz'))?$s:$_)."\n"}(1..30);

どれだけ短く書けるか競争なら、こうかな? 70 文字。読みにくい...

三項演算子を用いている部分をより短く書けないか *3 と久しぶりにリファレンスを参照してみて、 // 演算子や given 文といったものが Perl 5 に追加されていることを知った。given が文でなく式なら嬉しいのだが。

C# 9.0 (別解)
Enumerable.Range(1, 30)
  .Select( i => (i % 3 == 0 ? "Fizz" : "") + (i % 5 == 0 ? "Buzz" : "") is var s && s.Length > 0 ? s : "" + i )
  .ToList().ForEach( x => Console.WriteLine(x) );

標準解は素直な書き方だが、3, 5, 15 の倍数での場合分けと行数が多くなってしまうところに若干の負けた感が漂うため、別解も用意してみた。作成した Fizz/Buzz 文字列を (is var パターンマッチのちょっとした濫用により) 変数 s で受け、ワンラインの式としている。

あるいは次の通り。switch と () が増える分だけ長くなるかと思いきや同じコード長。ポイントはやはり、代入文を使わず、式の内部で変数 s を受けるというところ *4

Enumerable.Range(1, 30)
  .Select( i => ((i % 3 == 0 ? "Fizz" : "") + (i % 5 == 0 ? "Buzz" : "")) switch { "" => "" + i, var s => s } )
  .ToList().ForEach( x => Console.WriteLine(x) );

コード長 + 可読性 の観点からはこれが、実行速度 + 可読性の観点からは標準解が C# の最適解 *5 になるだろうか。


パラダイムオブジェクト指向から関数型プログラミングに移りつつあってプログラミング言語が乱立する時代でもあり、たまにこういう基礎問題に立ち返るのも悪くない。

関数型が志向されるようになると式をワンラインで記述することも許容されやすくなるだろう。ワンライナーが嫌われるのは、意味単位の異なる複数ステートメントを無理やり1行に収めようとするからだと思っている。

おまけ

C# 9.0 ("世界のナベアツ" バージョン)
using System.Text.RegularExpressions;

var dic = new [] {
  "/じゅう/にじゅう/さぁ~んじゅう/よんじゅう"   ,
  "/いち/に/さぁ~ん/し/ご/ろく/しち/はち/きゅう",
}
  .Select( str => 
            str.Split("/")
              .Select( (x, i) => new { Key = i, Value = x } )
              .ToDictionary( x => x.Key, x => x.Value ) )
  .ToList();

Enumerable.Range(1, 40)
  .Select( i => i % 3 == 0 || Regex.IsMatch($"{i}", "3") ? dic[0][i / 10] + dic[1][i % 10] : $"{i}" )
  .Append("オモロ~")
  .ToList().ForEach( x => Console.WriteLine(x) );
// 出力結果
// 
// 1
// 2
// さぁ~ん
// 4
// 5
// ろく
// ...
// 29
// さぁ~んじゅう
// さぁ~んじゅういち
// さぁ~んじゅうに
// さぁ~んじゅうさぁ~ん
// さぁ~んじゅうし
// さぁ~んじゅうご
// さぁ~んじゅうろく
// さぁ~んじゅうしち
// さぁ~んじゅうはち
// さぁ~んじゅうきゅう
// 40
// オモロ~

*1:にわかプログラマが適当に書いた、設計思想も可読性/保守性/拡張性もないコードが散在すると業務効率が低下し、DX (= 業務変革) や経済発展は却って阻害されると個人的には思うのだが、それを懸念する声はいまのところ聞こえてこない。自己流の業務プロセスにこだわり、パッケージよりもスクラッチ/アドオン開発を志向して出遅れたという日本企業の 2000 年ごろの ERP 導入の轍を踏む気がする。

*2:Perl では false のみならず undef, 空文字列, 0 も偽と判定されるため、これを利用して演算をうまく短絡表現できるか検討するのが面白いのだが、たいてい期待外れに終わる。

*3:A ? A : B の A が二度出現しないような短絡表現できる (例えば A ?: B のような) 演算子が導入されたら、代入による中間変数宣言が不要になり、(関数型プログラミング観点での) 表現力向上に資すると思う。

*4:LINQ to Entities でビジネスロジックをゴリゴリ書いていると、手続・文を用いずにいかに式のみで演算を記述するかという工夫を常にすることになるため、式内の変数受けは is var か switch 式を使う (あるいは .Select() メソッドを多段に連ねる) というのが定石として頭に入っている。残念ながら、パターンマッチは IL レイヤーで純粋関数ではない順次実行手続にコンパイルされる (= 式木 3.0 非対応) というのが .NET の仕様だと思われるため、is var も switch 式も LINQ to Entities では使えず LINQ to Object 限定になるのだが。

*5:結果をリストにするまでを 100 万回繰り返したときの所要時間は、標準解:別解 1:別解 2 = 625ms : 795ms : 835ms = 100%:127%:136% となった。オプティマイザ次第で揺らぐ要素が多々あるロジックであるため、実行速度の違いは無視できる水準ではある。