F# で Cmdlet を書いてる pt.64
krymtkts/pocof 開発をした。
昨日 0.19.0 をリリースをした。 1 月中にリリースしたかったが、 Linux で予期せぬエラーが発生する修正をしてしまってたので、リリースが遅れた。
ずっと気づいてなかったが、 Linux 端末上で例えば UnixStats
1 のような platform 依存の property にアクセスすると以下のエラーが発生する。
以下の例では Size
だが User
とか Mode
とか UnixStats
に属するもの全てエラーになる。
PS /mnt/c/Users/takatoshi> $ErrorView = 'DetailedView'
PS /mnt/c/Users/takatoshi> get-ChildItem | pocof -Layout TopDownHalf ':Size 1'
query>:Size 1 match and [0]
Exception :
Type : System.AggregateException
InnerExceptions :
Type : System.Management.Automation.GetValueInvocationException
ErrorRecord :
Exception :
Type : System.Management.Automation.ParentContainsErrorRecordException
Message : Exception getting "Size": "There is no Runspace available to run scripts in this thread. You can provide one in the DefaultRunspace property of the System.Management.Automation.Runspaces.Runspace type. The script block you attempted to invoke was: $this.UnixStat.Size"
HResult : -2146233087
CategoryInfo : NotSpecified: (:) [], ParentContainsErrorRecordException
FullyQualifiedErrorId : ScriptgetValueInvalidOperationException
TargetSite :
Name : InvokeGetter
DeclaringType : psscriptproperty
MemberType : Method
Module : System.Management.Automation.dll
Message : Exception getting "Size": "There is no Runspace available to run scripts in this thread. You can provide one in the DefaultRunspace property of the System.Management.Automation.Runspaces.Runspace type. The script block you attempted to invoke was: $this.UnixStat.Size"
InnerException :
Type : System.Management.Automation.PSInvalidOperationException
ErrorRecord :
Exception :
Type : System.Management.Automation.ParentContainsErrorRecordException
Message : There is no Runspace available to run scripts in this thread. You can provide one in the DefaultRunspace property of the System.Management.Automation.Runspaces.Runspace type. The script block you attempted to invoke was: $this.UnixStat.Size
HResult : -2146233087
CategoryInfo : InvalidOperation: (:) [], ParentContainsErrorRecordException
FullyQualifiedErrorId : ScriptBlockDelegateInvokedFromWrongThread
TargetSite :
Name : GetContextFromTLS
DeclaringType : scriptblock
MemberType : Method
Module : System.Management.Automation.dll
Message : There is no Runspace available to run scripts in this thread. You can provide one in the DefaultRunspace property of the System.Management.Automation.Runspaces.Runspace type. The script block you attempted to invoke was: $this.UnixStat.Size
Source : System.Management.Automation
HResult : -2146233079
StackTrace :
at System.Management.Automation.ScriptBlock.GetContextFromTLS()
at System.Management.Automation.ScriptBlock.InvokeWithPipe(Boolean useLocalScope, ErrorHandlingBehavior errorHandlingBehavior, Object dollarUnder, Object input, Object scriptThis, Pipe outputPipe, InvocationInfo invocationInfo, Boolean propagateAllExceptionsToTop, List`1 variablesToDefine, Dictionary`2 functionsToDefine, Object[] args)
at System.Management.Automation.PSScriptProperty.InvokeGetter(Object scriptThis)
Source : System.Management.Automation
HResult : -2146233087
StackTrace :
at System.Management.Automation.PSScriptProperty.InvokeGetter(Object scriptThis)
at Pocof.Query.x@162.Invoke(Entry entry)
at lambda_method453(Closure, Entry)
at System.Linq.Parallel.OrderPreservingPipeliningSpoolingTask`2.SpoolingWork()
at System.Linq.Parallel.SpoolingTaskBase.Work()
at System.Linq.Parallel.QueryTask.BaseWork(Object unused)
at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
...
RunSpace
割あたってると思ってたけどなんか違うらしい。
PSCmdlet.InvokeCommand.InvokeScript
を main thread で実行してるだけなので、変なことなさそうなんやが。
実は 0.18.1 以前からのエラーは出てて、単に try ... with ...
でエラーを捕捉した場合に property の値が取れなかったとして None
を返す仕様にしてたので気づいてないだけだった。
このエラー措置は元々 NullReferenceException
用に作ってた気がしたけど、結果的に他のエラーも救ってたことになる。
0.19.0 に含まれる修正で NullReferenceException
の恐れがないとみなしこのエラー救護措置を取り除いたところ、ひょっこり現れたという形だ。
ひとまず従来の挙動とリリース優先を優先して try ... with ...
でエラーを潰しただけなので、できればちゃんと対応したいところ。
あとの修正は、依存関係を更新したり、 benchmark とって改善したりだけだ。 改善といっても大きくロジックを書き換えたのはなくて不要な list 変換を避けるとかそういうちまちましたものばかり。 この過程で ILSpy で面白いことに気付いたので書き残しておく。
もし速さを追求して IL の命令数まで意識する必要がある場合、 F# っぽい書き方は C# っぽい書き方より overhead があって使いにくいかも知れないという話。
> dotnet --version
9.0.102
> dotnet fsi --version
Microsoft (R) F# Interactive version 12.9.101.0 for F# 9.0
でビルドして、 ILSpy で C# 12 と一部 IL で見た。
また pocof の TargetFramework
は netstandard2.0
だ。
F# が吐く IL は version up で改善が続いてるし、この見解は今だけの話かもしれないことに留意すること。
こういうコードがあるとして、 toString __ |> String.lower
の部分の書き方で生成される IL の違いを見た。
[<RequireQualifiedAccess>]
[<NoComparison>]
[<Struct>]
type Matcher =
| Eq
| Like
| Match
override __.ToString() = toString __ |> String.lower
// toString __ |> String.lower
public override string ToString()
{
string s = toString(this);
return LanguageExtension.String.lower(s);
}
// _.Property shorthand
public override string ToString()
{
string text = toString(this);
string text2 = text;
return text2.ToLower();
}
.method public hidebysig virtual
instance string ToString () cil managed
{
.param [0]
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = (
01 00 00 00 00
)
// Method begins at RVA 0x9238
// Header size: 12
// Code size: 21 (0x15)
.maxstack 3
.locals init (
[0] string 'Pipe #1 input at line 235',
[1] string
)
IL_0000: ldarg.0
IL_0001: ldobj Pocof.Data/Matcher
IL_0006: call string Pocof.Data::toString<valuetype Pocof.Data/Matcher>(!!0)
IL_000b: stloc.0
IL_000c: ldloc.0
IL_000d: stloc.1
IL_000e: ldloc.1
IL_000f: callvirt instance string [netstandard]System.String::ToLower()
IL_0014: ret
} // end of method Matcher::ToString
inline expansion と _.Property shorthand は同じ IL になる。
// inline
public override string ToString()
{
string text = toString(this);
string text2 = text;
return text2.ToLower();
}
C# ライクに書くのが最も命令数が少ない。
// (toString __).ToLower()
public override string ToString()
{
return toString(this).ToLower();
}
.method public hidebysig virtual
instance string ToString () cil managed
{
.param [0]
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = (
01 00 00 00 00
)
// Method begins at RVA 0x9238
// Header size: 1
// Code size: 17 (0x11)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldobj Pocof.Data/Matcher
IL_0006: call string Pocof.Data::toString<valuetype Pocof.Data/Matcher>(!!0)
IL_000b: callvirt instance string [netstandard]System.String::ToLower()
IL_0010: ret
} // end of method Matcher::ToString
以下の >>
と |>
の結果から、関数合成は遅いという話は本当なんだ。
また、 pipeline 演算するだけで余計にコピーしてるのはちょっとショック。
// >>
public override string ToString()
{
Matcher matcher = this;
FSharpFunc<Matcher, string> @_instance;
@_instance = ToString@235.@_instance;
FSharpFunc<string, string> @_instance2;
@_instance2 = ToString@235-1.@_instance;
Matcher matcher2 = matcher;
return @_instance2.Invoke(@_instance.Invoke(matcher2));
}
// |>
public override string ToString()
{
Matcher x = this;
string text = toString(x);
string text2 = text;
return text2.ToLower();
}
.method public hidebysig virtual
instance string ToString () cil managed
{
.param [0]
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = (
01 00 00 00 00
)
// Method begins at RVA 0x9238
// Header size: 12
// Code size: 23 (0x17)
.maxstack 3
.locals init (
[0] valuetype Pocof.Data/Matcher 'Pipe #1 input at line 235',
[1] string 'Pipe #1 stage #1 at line 235',
[2] string
)
IL_0000: ldarg.0
IL_0001: ldobj Pocof.Data/Matcher
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: call string Pocof.Data::toString<valuetype Pocof.Data/Matcher>(!!0)
IL_000d: stloc.1
IL_000e: ldloc.1
IL_000f: stloc.2
IL_0010: ldloc.2
IL_0011: callvirt instance string [netstandard]System.String::ToLower()
IL_0016: ret
} // end of method Matcher::ToString
IL の命令数を極限まで減らしていこうと pocof はしてないのだけど、 _.Property shorthand は気軽に使える高速な方法ってことで良さそう(実際そうした)。
逆にこれの結果に従うと C# like に書ける箇所は .
で引きずり回す方が効率が良いのでそうしようとなってしまうが、 pipeline の方が好きなので当面は採用しないでおく。
極限まで削らないといけないところが出たら局所的に採用するかも知れんけど。
最近 プログラマーのための CPU 入門 CPU は如何にしてソフトウェアを高速に実行するか を読んでるのもあって命令数に敏感になってるから気になってるだけって感じではある。 もっと全体的に見直す価値ある箇所あるのではよそっちやれ、という自戒。
-
UnixStat
のことはよく知らないが、PowerShell のコードにちらっと現れる。こいつを掘り下げて理解するのが今後の宿題かな ↩