おぼえられない

久しぶりに触っているJavaで諸々調査.試したいことを試すのに,しょーもないところからいちいちいぐぐらないといけないのが悲しい.int型をTextViewに表示するのどうやるの?とか,ディレクトリパスとファイル名を繋げるのどうやるの?とか,テキストファイルの出力ってどうやるの?とか.

らんだむ

PCとTVをオーディオケーブルで繋ぎ,アニメの主題歌や挿入歌をキャプチャし,歌詞を手打ちし,アーカイブするのが趣味?のはず?です.始めたのは恐らく23年前,高専に入学し,初めてのPCを買い,周りの連中の影響でアニヲタイズした頃です.現時点では約7600曲が溜まっています.

一覧をExcelファイルで管理しています.このExcelのマクロで,以下3種類のデータを更新・生成しています.

  • 歌詞HTML(ヘッダの曲名等を更新)やMP3へリンクするHTML(番組名等含む表形式)
  • コレクションしているリストのHTML(ホームページ掲載用)
  • PC操作中にBGMとしてランダム再生するプレイリスト

■プレイリスト生成ロジック改良

今回,プレイリスト生成のロジックを改良しようと思い立ちました.現在は,プレイリストに追加するかどうかを示すフラグの列を設け,そこが「O」になっているMP3ファイルのパスのみをプレイリストへ出力するようにしています.このフラグは,ホームページへ掲載するリストHTMLを更新するタイミングで,1年以上前の曲を「X」に変える運用にしています.これにより,直近1年の曲のみが再生されることになります.

アニソン管理ファイル
アニソン管理ファイル

しかし,たまにはもっと昔の曲と再会し,懐かしくなりたいなと.そこで,以下の要領でピックアップした曲をプレイリストへ出力したいと思います.

  • 直近6箇月は全て登録する.
  • それ以前は古いほど少なく,新しいほど多くなるような割合でランダムに採用する.
  • 全部で1000曲をプレイリストへ出力する.

■収録日の一覧化

そのためにはまず,収録日を一覧化する必要があります.大量なので,いちいち調べて手入力するのは非現実です.それだけで心折れちゃう.いろいろ思案した結果,MP3ファイルの最終更新日をピックアップすれば良いのではと.こんな単純なことに,なかなか思い至れませんでした.灯台下暗し.

マクロでの実装は簡単.最終更新日というか,ファイルの新しさが知りたいので,最古のデータを0とした月数を算出するようにしましょう.

Const AGE_ORIGIN As Integer = 2002 * 12 + 3
Dim fso As New FileSystemObject
Dim f   As File
Dim age As Integer
Set f = fso.GetFile("xxx.mp3")
age = Year(f.DateLastModified) * 12 + Month(f.DateLastModified) - AGE_ORIGIN

VBAがあまりに久しぶりすぎて,参照するライブラリ名「Microsoft Scripting Runtime」を思い出せませんでした.最古のデータは2002年3月で団子状になっていました.MP3なるものが出回り始めたこの頃,Wave(PCM)から一気にまとめて変換したんでした.これだとそれ以前の収録日情報が不正確になりますが,そこまでの精度は不要でしょう.

■加重ランダム採用

難しいのは,新しさに応じた重みでランダム抽出する方法です.VBAのRndメソッドが返す0以上1未満の乱数は,一様分布に従うものだと思います.これをどう活用するか.三つの方法を考えました.以下の5曲が候補に残っていて,そこから3曲を選ぶ例で説明したいと思います.

  • 曲A:月数1
  • 曲B:月数1
  • 曲C:月数2
  • 曲D:月数2
  • 曲E:月数4

■加重ランダム採用方法#1

最初に考えたのは,乱数を一つ算出し,それに対応する積算月数の曲を採用する,というのを繰り返す方法です.

月数積算月数採用条件採用率
A11Rnd*10<110%
B12 = 1+11≦Rnd*10<210%
C24 = 1+1+22≦Rnd*10<420%
D26 = 1+1+2+24≦Rnd*10<620%
E410 = 1+1+2+2+46≦Rnd*10<1040%

Rndメソッドの戻りが0.5だった場合,曲Cが採用されます.そして次の1曲は,以下の方法で採用されます.という風に繰り返す方法.

月数積算月数採用条件採用率
A11Rnd*8<112.5%
B12 = 1+11≦Rnd*8<212.5%
D24 = 1+1+22≦Rnd*8<425%
E48 = 1+1+2+44≦Rnd*8<850%

この方法の問題は,処理時間がかかることです.1曲選ぶたびに積算月数を再計算する必要があります.計算量は候補数×採用数のオーダーです.実行に1分以上かかりました.これは嫌.

■加重ランダム採用方法#2

次に考えたのは,月数に基づく確率で各曲の採用是非を調べていく方法です.

月数採用条件採用率
A1Rnd<0.1 = 1/(1+1+2+2+4)10%
B1Rnd<0.1 = 1/(1+1+2+2+4)10%
C2Rnd<0.2 = 2/(1+1+2+2+4)20%
D2Rnd<0.2 = 2/(1+1+2+2+4)20%
E4Rnd<0.4 = 4/(1+1+2+2+4)40%
  1. 乱数を算出し,0.1未満だったら曲Aを採用する.
  2. (曲Aを採用したかどうかに関わらず)乱数を算出し,0.1未満だったら曲Bを採用する.
  3. (曲AやBを採用したかどうかに関わらず)乱数を算出し,0.2未満だったら曲Bを採用する.
  4. 以下同様

この方法だと,一巡では採用数に達しないことが多いです.実際は分母がもっともっと大きいので,なかなかこの採用条件が満たされないのです.そのため,方法1ほどではないにせよ,何度か回す必要があります.例えば,1巡目で曲BとDが採用された場合は,2巡目で下表のチェックをしていくことになります.

月数採用条件採用率
A1Rnd<0.1 = 1/(1+2+4)14%
C2Rnd<0.2 = 2/(1+2+4)29%
E4Rnd<0.4 = 4/(1+2+4)57%

やっぱり処理時間,かかりました.全部で600巡くらい必要で,十数秒かかりました.これもちょっと嫌.

■加重ランダム採用方法#3

最後に考えたのは,方法2をベースに,残採用数が多いほど採用条件を緩める方法です.

  1. 曲Aの採用条件を「Rnd<0.3 = 1/(1+1+2+2+4)*残採用数3」とする.
  2. 乱数が0.3未満だった場合
    1. 曲Aを採用する.
    2. 曲Bの採用条件を「Rnd<0.2 = 1/(1+1+2+2+4)*残採用数2」とする.
  3. 乱数が0.3以上だった場合
    1. 曲Aを採用しない.
    2. 曲Bの採用条件を「Rnd<0.3 = 1/(1+1+2+2+4)*残採用数3」とする.

この方法でも,一巡では採用数に達しませんでした.何度か回す必要があります.とはいえ,加速項のお陰で,5巡くらいで済みました.処理時間は数秒程度.いいね.

こうして方法3でプレイリストに採用された曲の月数統計をグラフ化しました.良い感じです.頭痛に見合う価値があります.

月数に対する採用率
月数に対する採用率

みたいなことを眠い中やっつけるのが会社での私の仕事の一つです.上記の数理統計学的な理論立てが合っていたとしても,プログラムへの落とし込みに間違いがあると台無し.眠い中,どっちに誤りがあるのか探したりするの辛い.どっちもが合っていたとしても,目標とする結果に達していなければ,別の案を捻りだす必要があります.フレーフレー私.

いきなりみどり

数列の平均と標準偏差を算出し,任意の平均および標準偏差の数列へ変換するメソッドを作ろうとしました.元の数列の平均と標準偏差は,処理過程の値として,参照渡しの引数へ突っ込んで返します.

static void f(double* data, int n, double* av, double* sd)
{
	const double AV_DST = 0.0;
	const double SD_DST = 1.0;
	int i;
	double coef, sum, sumSqr;

	sum = sumSqr = 0.0;
	for (i = 0; i < n; i++)
	{
		sum += data[i];
		sumSqr += data[i] * data[i];
	}
	*av = sum / n;
	*sd = (sumSqr - sum * sum / n) / n;

	coef = SD_DST / *sd; // ここ

	for (i = 0; i < n; i++) {
		data[i] = (data[i] - *av)*coef + AV_DST;
	}
}

下から4行目,係数を代入する式を書こうと,「coef=SD_DST/*」まで打った瞬間,以降の文字がいきなり緑になりました.そうか.コメント始まったか.

えんばん

\さいたま/をDVDに焼いてほしいとの依頼を受けました.しかし,自宅にある円盤はDVD-RAMだけ.読めないDVDプレーヤー,多い気がします.そーいや個人では滅多に使わないからと,DVD-Rは会社へ持っていった気がします.

デスクの引き出しに,スピンドルケースに入った約20枚の円盤が眠っていました.鞄に突っ込んで帰宅.

開けてびっくり玉手箱.記録面が青くない.薄緑.これDVD-Rちゃう,CD-Rやんか.そーいや,個人では滅多に使わないという理由に加え,数MB程度のファイルの納品に4.7GBのDVD-Rを使うのが勿体ないという理由もあって,の私用円盤供出でした.骨折り損の草臥れ儲け.プラスチックの塊,また会社へ.

てぬき

今朝書いた昨日分の日記,調べ物など気合入れたご褒美に,今日の分は手抜きします.家の別のPCのスペックも比較表にしてみようかと.新しいOSに対応していないプリンタで印刷するのと,録り溜めアニメをDVD-RAM経由でリッピングするため,だけに使っているPCです.苦手な調べ物をわざわざしたため,労力的には手抜きになりませんでした.結局,対応する項目が調べきれていませんが,下表な感じ.

自宅Desktop新自宅Desktop旧
CPU名Celeron G3900Celeron E1400
発売年2015年2008年
コア数2コア/2スレッド2コア/2スレッド
基底周波数2.8GHz2.0GHz
キャッシュ容量2MB512KB
RAM容量4GB2GB
OSWindows10WindowsVista

ひかくひょうすきです

先日,自宅DesktopPCでのテレワークで,のろのろ運転を体験しました.計画テレワークの際は,会社LaptopPCを持ち帰るようにしています.そのPCでは,あたかも会社DesktopPCで作業しているかのように,さくさく運転です.

将来の自宅PC買い替えの参考のため,この速度違いが何に由来するのか,調べてみようと思いました.ガンガンにHDDアクセスしている風ではないので,HDDの回転数はあまり関係ない気がします.複数アプリの同時起動を嫌っているので,RAMもあまり関係ない気がします.実際タスクマネージャを見ても逼迫していない感じですし,常駐サービスはむしろ会社PCの方が多くて重そうです.となると,やはりCPUが鍵なんでしょう.

学生時代は,周波数が処理速度とほぼ相関するものと思っていました.ところがもはや周波数競争は飽和し,やがてはコア数競争になった感じ.でも,周波数とコア数以外にも,性能に帰する要素はたくさんあるでしょう.

自宅Desktop会社Desktop会社Laptop
CPU名Celeron G3900Core i7-3770Core i7-7500U
Intel世代第6世代Skylake第3世代Ivy Lake第7世代Kaby Lake
発売年2015年2012年2016年
希望小売価格4,600円31,700円43,100円
コア数2コア/2スレッド4コア/8スレッド2コア/4スレッド
基底周波数2.8GHz3.4GHz2.7GHz
キャッシュ容量2MB8MB4MB
バス転送速度8GT/s5GT/s4GT/s
RAM容量4GB8GB8GB
OSWindows10Windows8Windows10

というわけで,その辺を調べてまとめました.比較表好きです.実効的な処理性能としては,「自宅Desktop<会社Desktop≒会社Laptop」という感じです.

この方程式と上表を対応付けると,RAM容量,やっぱり関係あるのかしら.ま,RAM が逼迫しないよう遅いHDDでページングし,その上での使用率をタスクマネージャに表示している,ってこともあるでしょうからね.

あとはIntelの世代かしら.ここ違うと結構性能差ありそう.とかいろいろ思いましたが,結局そもそもの話,廉価指向のCeleron/性能指向のCore i7の違いが最大要因でしょう.もはや土俵が違うレベルか.

あうゑいと

C#で非同期メソッドを呼び出す際に使うawaitステートメント,この挙動を誤解していました.非同期処理を同期的な順で処理しつつ元スレッドを専有しない便利なやつ,という認識でしたが,ちょっと違うぽ.私が誤解していたawaitの挙動は以下のとおりでした.

  1. 別スレッドでタスクを開始する.
  2. 元スレッドは一旦解放される.(だから元スレッドでの他のタスクが動かせる)
  3. 別スレッドでの処理が終わったら,元スレッドでawaitの続きを処理する.

正しいawaitの挙動は以下のとおりだったようです.

  1. 別スレッドでタスクを開始する.
  2. 元スレッドは一旦そこでメソッドを抜ける.(だから呼出元の続きの処理が並行で消化される)
  3. 別スレッドでの処理が終わったら,元スレッドでメソッド内のawaitの続きを処理する.

大抵,非同期メソッドをawaitで呼ぶ非同期メソッドは,それ自体もawaitで呼ばれます.この場合の挙動は正解と誤解に差がないため,今まで気づけないでいました.

自戒を込め,気合い入れて例示してみます.まず,普通の例.ま,こうなるよね.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Task _ = MethodA();
        }

        static async Task MethodA()
        {
            Console.WriteLine("MethodA started.");
            await MethodB();
            Console.WriteLine("MethodA finished.");
        }

        static async Task MethodB()
        {
            Console.WriteLine("MethodB started.");
            await Task.Run(() =>
            {
                Console.WriteLine("Run started.");
                Thread.Sleep(3000); // HeavyTask
                Console.WriteLine("Run finished.");
            });
            Console.WriteLine("MethodB finished.");
        }
    }
}
実行結果:
MethodA started.
MethodB started.
Run started.
(ここで3秒待ち)
Run finished.
MethodB finished.
MethodA finished.

そして,これが私の理解と違った例.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Task _ = MethodA();
        }

        static async Task MethodA()
        {
            Console.WriteLine("MethodA started.");
            Task _ = MethodB(); // <-------- ここを変えた
            Console.WriteLine("MethodA finished.");
        }

        static async Task MethodB()
        {
            Console.WriteLine("MethodB started.");
            await Task.Run(() =>
            {
                Console.WriteLine("Run started.");
                Thread.Sleep(3000); // HeavyTask
                Console.WriteLine("Run finished.");
            });
            Console.WriteLine("MethodB finished.");
        }
    }
}
実行結果:
MethodA started.
MethodB started.
MethodA finished.
Run started.
(ここで3秒待ち)
Run finished.
MethodB finished.

誤解していた私は,「普通の例」との違いが「MethodBの処理中に元スレッドで他のタスクが動かせるかどうか」だけで,ゆえに同じ実行結果になると思っていました.つまり,思っていた動きは以下のとおりです.

  1. MethodA started.
  2. 元スレッドでMethodB started.
  3. 別スレッドで3秒かかる処理.元スレッドは別スレッドの処理完了待ち.
  4. 3秒後,元スレッドでMethodB finished.
  5. 呼出元のMethodA finished.

で,正しい動きはなるほど,以下のとおりとなるわけです.

  1. MethodA started.
  2. 元スレッドでMethodB started.
  3. 別スレッドで3秒かかる処理開始.元スレッドはMethodBを抜けて(中断して)呼出元の続きのMedhodA finished.
  4. 3秒後,元スレッドでMethodB finished.

すぴる

自宅のPCのOffice365のExcelがいつの間にか更新されていました.「スピル」って何? ググりました.配列形式が返ってくる関数に関するものらしいです.

従来は,埋め先の複数セルを選び,Ctrl+Shift+Enterを押すことで,計算結果を複数セルへ出していました.ところが,新しいExcelでは,そんなことしなくても,起点のセルに数式を入れることで,隣接セルが動的に変わるらしいです.これはまあ便利.

でも,逆に隣接セルを変えたくない場合はどうしたら良いのか.その場合は数式の前に「@」を付けるらしいです.こうすると従来同様,入力したセルにしか値が入らなくなります.なるほど.

専らExcel使いですし,毎年新人研修でExcel等を教える係になっているので,この辺は一応押さえておきたいところ.

まてる

朝から客先直行.乗れない電車を警戒し,少し早めに出ました.そして早めに着きました.待つ間,メールやビジネスチャットをチェック.そーいや1箇月前,のっけから200kbpsで行く通信プランに変えました.たしかに,明らかにメール等の表示が遅くなりました.でも,この程度なら待てます.ありです.急いでいるときは苛々しますが,そのときは速いプランでも同様でしょう.

むずかしくないはずなのに

カメラのライブラリを呼び出し,画像を一コマ一コマ順次取得し,フロントアプリへ投げ,動画のように表示させるためのライブラリを作っています.似たようなのを何度も作ったことがあるのに,いや,あるがゆえに簡単にできそうだと思っているためにか,なかなかうまく行かずに嫌になっちゃう.

カメラのライブラリはC言語で呼び出します.一方,表示アプリはC#/WPFです.そのため,繋ぎの自作ライブラリはC++/CLIで作っています.開始メソッドを呼んだら,画像の取得と転送を別スレッドで行います.C言語寄りのところでは関数ポインタが登場し,C#寄りのところではデリゲートが登場します.C言語寄りのところでは画像情報をmallocしたアンマネージドなunsigned char*型で保持したりしますが,C#寄りのところではマネージドなarray<unsigned char>^型で保持したりします.速度面ではなるべくアンマネージドが良いけど,mallocとfreeをちゃんとやらないとメモリリークするとか,CLIだとstd::threadが使えないなど,いろいろあります.

の,どこまでをどっち流に書くか,プログラムの構成含めてトライアルアンドエラーでスクラップアンドビルドでした.さらに,C言語とC++とC++/CLIとC#を,それぞれちゃんと理解していないと,たちまち書き間違えて怒られる罠.テトリスだわ.