同一ファイルに対して、 複数種類のファイルハッシュを高速に計算する

ネットワークドライブ上の GB オーダーのファイルに対して、複数のハッシュアルゴリズムでファイルハッシュを計算する必要に駆られていた。
「そんなシチュエーションが本当にあるのか?」と思うかも知れないが、あったのだから仕方ない。

最初は、 PowerShell を使って 愚直に Get-FileHash を 2回 計算していた。
しかし当たり前だが、 2回ファイルをダウンロードすることになるこの方法は遅くて仕方ない。

ということで、 C# でコンパイルしたコード上で、 一度メモリに読み込ませてから複数のハッシュアルゴリズムで計算できるようなコードを作成した。

バッファーサイズを 80KiB 程度とすると、 .NET の仕様で 85KB を境にメモリの扱いが変わる (LOH と呼ばれる特別なヒープに移動する) ことから、 このサイズを超えると一般的に動作速度が下がると言われている。
しかし、今回のコードの場合バッファーが小さいと、 Task 周りの処理がボトルネックになってしまうので、 8MiB くらいのサイズとしている。

80KiB としているのは、 .NET mscorlib の System.IO.Stream.CopyTo メソッドなどでもおなじみなのだが、これが必ずしも正解とは限らないわけだな。


パフォーマンス

800Mbps 位でシーケンシャルリードできるリモート上の 4GB のファイルを、 4回 ずつ計測した平均を計測した。
Get-FileHash の 2種類目 以外はクライアントキャッシュが効かないように注意し、 計測の各回でハッシュを計算するファイルはそれぞれ別のファイルで、中身はランダムバイナリとした。
Get-FileHash については、 計測の各回では同じファイルに対して -Algorithm パラメータを変えて複数回連続で呼び出した。

計算したハッシュの種類 Get-FileHash 上記の改良スクリプト
1種類 (SHA1) 48.8s 43.3s
2種類 (MD5, SHA1) 60.4s 44.2s
3種類 (MD5, SHA1, SHA256) 83.3s 48.2s

ん? 計算したハッシュが 1種類 の場合でも、改良スクリプトの方が早いぞ?


うわっ… HashAlgorithm.ComputeHash の実行速度、低すぎ…?

PowerShell v5 相当

PowerShell v6 相当

上記のように、 Get-FileHash は、内部的に HashAlgorithm.ComputeHash でハッシュ計算を実行している。

で、その HashAlgorithm.ComputeHash がどうなっているかというと、 4KiB 毎に ファイルの読み込みと、 ハッシュストリームへの書き込みを 同期的に 行っている。

ファイルの読み込みも、 (使うアルゴリズムにもよるけど) ハッシュの計算も、 どちらもコストがかかるので、同期的にやってたらそりゃ遅いわ。

ZoneId を PowerShell を使って一括削除 [v3 以上]

Windows では、リモートからダウンロードしたファイルに、 "ZoneId" という物が付加される。
このID が付加されたファイルは、 exe として 実行する際にセキュリティ警告が表示されたり、 .NET アセンブリ dll として PowerShell に読み込む際に、

Add-Type : ファイルまたはアセンブリ 'file:///*.dll'、またはその依存関係の 1 つが読み込めませんでした。操作はサポートされません。 (HRESULT からの例外: 0x80131515)
発生場所 行:1 文字:1
+ Add-Type -Path 'C:\*.dll'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Add-Type], FileLoadException
    + FullyQualifiedErrorId : System.IO.FileLoadException,Microsoft.PowerShell.Commands.AddTypeCommand

などとエラーになってしまう。
160925_1

ファイルが一つだけなのであれば、ファイルのプロパティから『ブロックの解除』をチェックすれば、簡単に ZoneId を削除することができる。
160925_2
しかし、複数個のファイルの ZoneId を一回の操作で同じように削除することができないため、 ZoneId を削除したいファイルが大量にある場合はなかなかウザい。

PowerShell v3 以上を使えば、この ZoneId 削除をいとも簡単に行うことができるので、紹介しよう。

続きを読む

PowerShell バージョンごとのスクリプティング機能の新機能概略

PowerShell v3 以降のそれぞれのバージョン毎のスクリプティングを中心とした新機能のうち、私の独断で選んだ主な機能を列挙してみた。
(PowerShell の本来の役割のはずの) コンピュータ管理機能などはばっさり省いている。

意外と、そういう情報まとまっているのを見たことなかったので。。。

続きを読む

PowerShell の組織の中で使えそうで使えないニッチな Tips

この記事は、 PowerShell Advent Calendar 2015 の 18日目 の記事だ。
私はこれまでずっとネットヒッキーだったので、ネット上でほかの人とのかかわりを持ったことがほとんどなかったが、今回意を決して参加させてもらうことになった。

バリバリの 情シス や プログラマ ではなくても、日常のちょっとした業務で PowerShell を使っている人もいるだろう。
そういったひとびと(主に私)が、組織の中で使えそうだと思ったニッチな話題を書こうと思う。

  • PowerShell Tools for Visual Studio を商用利用する環境は、無料でできる
  • コンソールウィンドウを極力表示せずにスクリプトを実行
  • ネットワーク共有上の DLL を読み込む

続きを読む

SQLite の フィールド数 によるパフォーマンス

対策:SQLite データベースのパフォーマンスの最適化 - Data Storage - 開発ガイド - BlackBerry Java SDK - 6.0 に、「行のサイズを最小限にする: 幅の広い列がある場合、個別のテーブルにすることを検討します。」 なんて改定あるので、実際のところ行サイズはどれぐらいパフォーマンスに影響するのか気になってきた。

せっかくなので、レコードのデータをすべてフィールドにしてしまった場合と、すべて別テーブルにしてしまっている場合と、どちらがよいパフォーマンスを示すのかについて、 RAM メモリ, SSD, HDD でそれぞれ読み書き速度を比較してみた。

あくまで、私の環境においてなので、参考ぐらいにしておいていただければ幸いだ。

検証コード

一応、以下の様な配置で、bundle 版 System.Data.SQLite.dll を配置していることが前提。

続きを読む

PowerShell で 配列をシャッフルする

コード

PowerShell で 極力簡単に配列をシャッフル (ランダムにソート) する方法。
速度や制度などはガン無視して、簡単に書けることを重視。

$shuffled = $array | sort -prop @{Exp={[Guid]::NewGuid()}}

ハイ終わり。

検証

ホントに実用的なシャッフルになっているか心配なので、実際に 0~19 のシャッフルを 2000 回行って、先頭に来た数字と、10 が来たインデックスを集計してみる。

$r = 0..1999
$r | %{ $r[$_] = 0..19 | sort -prop @{Exp={[Guid]::NewGuid()}}; }
$r | %{ $_[0] } | group | Format-Table -AutoSize
$r | %{ $_.IndexOf(10) } | group | Format-Table -AutoSize
Count Name Group
----- ---- -----
  104 16   {16, 16, 16, 16...}
   91 19   {19, 19, 19, 19...}
  111 8    {8, 8, 8, 8...}
  102 7    {7, 7, 7, 7...}
   95 18   {18, 18, 18, 18...}
  105 3    {3, 3, 3, 3...}
   95 12   {12, 12, 12, 12...}
   83 15   {15, 15, 15, 15...}
  100 17   {17, 17, 17, 17...}
   89 5    {5, 5, 5, 5...}
  103 13   {13, 13, 13, 13...}
  103 11   {11, 11, 11, 11...}
  101 10   {10, 10, 10, 10...}
  110 4    {4, 4, 4, 4...}
  100 6    {6, 6, 6, 6...}
   88 2    {2, 2, 2, 2...}
  123 9    {9, 9, 9, 9...}
   92 1    {1, 1, 1, 1...}
  107 0    {0, 0, 0, 0...}
   98 14   {14, 14, 14, 14...}

Count Name Group
----- ---- -----
  102 17   {17, 17, 17, 17...}
   93 15   {15, 15, 15, 15...}
  100 9    {9, 9, 9, 9...}
  109 18   {18, 18, 18, 18...}
   91 4    {4, 4, 4, 4...}
  104 5    {5, 5, 5, 5...}
  111 3    {3, 3, 3, 3...}
  107 7    {7, 7, 7, 7...}
   83 1    {1, 1, 1, 1...}
  106 10   {10, 10, 10, 10...}
  101 13   {13, 13, 13, 13...}
   93 8    {8, 8, 8, 8...}
  111 16   {16, 16, 16, 16...}
  101 0    {0, 0, 0, 0...}
   83 2    {2, 2, 2, 2...}
  104 14   {14, 14, 14, 14...}
  103 12   {12, 12, 12, 12...}
   98 11   {11, 11, 11, 11...}
   81 19   {19, 19, 19, 19...}
  119 6    {6, 6, 6, 6...}

だいたい 100 に収束しているし、問題ないんじゃない?

とはいえ、 Guid クラスは、一意性はある程度期待できると書かれているものの、ランダム性は特に言及されていない。
出現にパターンがある可能性もあるし、厳密にランダムである必要がある場合は、使わない方がいいかもしれない。

続きを読む

スクリプト言語などから、共有フォルダにある .NET アセンブリを完全信頼で読み込む

PowerShell や IronPython などで ネットワーク共有フォルダに置かれている .NET アセンブリを読み込むと、このアセンブリのパスが実行ファイルの起動パスと異なるため、部分信頼での実行となってしまい、ファイルの読み書きなどの多くのコードが実行できない。
参考: MSDN | イントラネット アプリケーションの完全信頼での実行

一旦バイナリファイルをメモリに読み込んでから、アセンブリとしてロードするなどと言う方法もあるが、アセンブリからの相対パスなどが利用できなくなったりと、若干使い勝手が悪くなってしまう。

その回避方法として、Vista 以降限定ながら、アセンブリのフォルダをローカルにシンボリックリンクを貼って、そのシンボリックリンクを経由してアセンブリにアクセスすれば、完全信頼として実行することができる。

例えば、複数の PC で同じスクリプトを実行する様な場合、バージョンアップするときに全部の PC をもれなく更新するなど環境をそろえるが面倒だ。
このようなとき、イントラネットの共有ディレクトリにスクリプト置いて、ローカルからシンボリックリンクを通して実行すれば、更新するときはネットワーク共有のスクリプトを書き換えるだけで済み、コードも完全信頼で実行されるので、なかなか便利になる。

サンプルコード

実際に nasne の共有ディレクトリに、ファイルの読み込みを行えるコードを持ったアセンブリを作成して、読み込んでみよう。

続きを読む

リモートデスクトップ時に、バッチでサーバ側のPCをスリープさせる

リモートデスクトップとかやっていると、WoL で遠隔起動はできるのに、コマンドプロンプトにも PowerShell にも、(休止じゃなくて)スリープができるコマンドがなくて困る。
そんなときに、Windows7 以降で標準搭載している PowerShwll を使って、Win32API を直呼び出しして、休止する方法をご紹介。

使い方はとっても簡単。
下記の一行を hoge.bat fuga.cmd などと、適当なバッチファイルにして保存するだけ。
後はダブルクリックすれば、その PC をスリープ状態にできる。

@echo (Add-Type TempClass -Member '[DllImport("powrprof.dll")]public static extern bool SetSuspendState(bool Hibernate,bool ForceCritical,bool DisableWakeEvent);' -PassThru)::SetSuspendState($false,$false,$false) | powershell -c -

呼び出している Win32API については、MSDN の記事を参照: SetSuspendState function (Windows)
「SetSuspendState を呼び出す C# の関数を、PowerShell でコンパイルし、PowerShell から呼び出すようになっているコード」を、コマンドプロンプトで echo し、実行コマンドとして PowerShell にパイプで渡しているだけだ。

やってることはめんどくさいが、Windows7 以降なら追加のツールのインストールとかも必要なくすぐに作れるのがミソ。
PowerShell + Add-Type は本当に何でもできるから困る。

ちなみに、ちまたで見かける

shellrundll32.exe PowrProf.dll,SetSuspendState 0,1,0

とか言うのは間違った使い方だ。
Rundll32.exe の仕様 を見ればわかるとおり、このアプリケーションは、特定のプロトタイプに従った関数を呼び出すためのもので、任意の関数を呼ぶためのものではない。
上記のような呼び出し方をすると、実際には

   hwnd = (parent window handle)
   hinst = HINSTANCE of PowrProf.dll
   lpszCmdLine = "0,1,0"
   nCmdShow = (whatever the nCmdShow was passed to CreateProcess)

としたうえで、

   SetSuspendState(hwnd, hinst, lpszCmdLine, nCmdShow)

という呼び出しをしていることになってしまう。
これはもちろん SetSuspendState の本来の引数である (BOOLEAN Hibernate, BOOLEAN ForceCritical, BOOLEAN DisableWakeEvent) の bool 値3つに換算すると、(true, true, true) となるため (hwnd, hinst, lpszCmdLine いずれも 0 にならないj)、意図したスリープ状態にならない。
それどころか、イベントやタイマ、WoL から起動できなくなったりする為、Rundll32.exe を使うのは避けた方がいいだろう。

SetSuspendState に限っては、呼ばれる関数に対して呼び出す引数の数(正確には引数の数ではなく、引数の合計サイズ)が多いので問題にはならないが、場合によってはメモリ破壊を引き起こすこともあり得るので、 Rundll32.exe の扱いには注意が必要だ。

非同期でプログラムを実行するバッチを、パイプする

start コマンドを使い、非同期でプログラムを実行するバッチを作成し、そのバッチの出力をパイプすると、実行したプログラムが終了するまでパイプの処理が返らない。

pause.cmd

@echo pause
@exit /b

process.cmd

@echo off
Echo 処理1開始
Rem 処理1
Echo 処理2開始
start "" cmd /s /c "pause.cmd"
exit /b

と作成し、コマンドプロンプトで単に

process.cmd

とすれば、pause.cmd を閉じなくても次の処理に進めるのだが、

process.cmd | sort

などとパイプしたとたん、処理が返らなくなってしまう。

これの対処法はいくつかある。 続きを読む

[Powershell] UTF-8 でエンコードされた標準出力を受け取る

2バイト目に ASCII コードがくることを想定していないコンソールアプリなど、標準出力を UTF-8 として使用するコンソールアプリは少なくない。
しかし、PowerShell は標準では UTF-8 の標準出力をそのまま読み取ることができない。
(後述するが、単純な方法はある)

そういった場合でも、.NET Framework を駆使すれば、ちゃんと文字列として取得できる。

続きを読む