编译 C# 脚本很慢
Compiling C# scripts is slow
我有一个使用 "scripts" 用 C# 编写的应用程序,在启动时使用 C# CodeDomProvider 编译。加载几十个,甚至超过 170 个脚本都不是什么大问题,我编译它们然后将程序集保存到缓存文件夹中以备下次启动时使用。对于 170 个脚本,此初始编译大约需要 1.5 分钟。
但是,当我尝试加载超过 1000 个脚本时,编译它们花了一个多小时。我添加了一个秒表并了解到每个脚本的加载时间都比之前的长一点,在 170 个文件的情况下,它从第一个文件的 ~150ms 到最后一个文件的 >650ms,每个文件都在一点点增加文件。
我知道我可以通过将脚本合并到一个大文件中来大大减少加载时间,但是出于多种原因,如果我可以单独编译它们,我会更喜欢它:/它使重新加载它们变得容易并且快,我不用担心当有什么变化时重新编译整个脚本文件夹,我可以很容易地给出一个编译进度的进度条等等
现在我的问题是,这里有什么问题?为什么每个文件的编译时间会随着时间的推移而增加?我可以做些什么吗?
编辑
我会尝试提供更多信息,如评论中的要求。
正如我所说,我正在使用 C# CodeDomProvider 编译每个脚本,基本上是循环中的以下内容,对于我当前的 171 个脚本文件中的每一个,每个文件包含一个或多个 类我在创建程序集后实例化:
var provider = System.CodeDom.Compiler.CodeDomProvider.CreateProvider("CSharp"); ;
var parameters = new System.CodeDom.Compiler.CompilerParameters();
parameters.ReferencedAssemblies.AddRange(AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic).Select(a => a.Location).ToArray());
parameters.GenerateExecutable = false;
parameters.GenerateInMemory = false;
parameters.OutputAssembly = tmpFileName + ".compiled";
parameters.TreatWarningsAsErrors = false;
parameters.WarningLevel = 0;
var results = provider.CompileAssemblyFromFile(parameters, tmpFileName);
asm = results.CompiledAssembly;
我使用 CS-Script 得到了相同的结果,它做了类似的事情。
asm = CSScript.LoadWithConfig(tmpFileName, null, debug, CSScript.GlobalSettings, "/warnaserror- /warn:0");
然后将创建的程序集保存到缓存文件夹中。当脚本文件比程序集旧时使用保存的程序集,所以我不必再次编译它。
当你坐在它前面时,它会随着时间的推移变得越来越慢,看着进度条越来越慢,所以我在上面的代码周围添加了一个简单的秒表调用,StartNew, Stop, Elapsed
,没有什么花哨。结果反映了我在进度条上看到的情况,每增加一个文件,编译时间就会增加。
timer.Restart();
// compile as shown above
timer.Stop();
Console.WriteLine(asm.Location + ": " + timer.Elapsed);
当然,文件之间会存在细微差异,具体取决于它们的大小和复杂程度,但脚本通常在大小和复杂性方面都或多或少相同,我正在目睹随着时间的推移持续增加。
Temp\tmpDDD7.tmp.compiled: 00:00:00.1389177
Temp\tmpDE74.tmp.compiled: 00:00:00.1327150
...
Temp\tmpE156.tmp.compiled: 00:00:00.1719746
Temp\tmpE213.tmp.compiled: 00:00:00.1431011
...
Temp\tmpF05C.tmp.compiled: 00:00:00.1696297
Temp\tmpF118.tmp.compiled: 00:00:00.1739564
...
Temp\tmpF7D5.tmp.compiled: 00:00:00.1824292
Temp\tmpF891.tmp.compiled: 00:00:00.1819889
...
Temp\tmp29F1.tmp.compiled: 00:00:00.2912163
Temp\tmp2B2B.tmp.compiled: 00:00:00.2909096
...
Temp\tmp362F.tmp.compiled: 00:00:00.3161408
Temp\tmp3773.tmp.compiled: 00:00:00.3170768
...
Temp\tmpA4C9.tmp.compiled: 00:00:00.5457990
Temp\tmpA6FC.tmp.compiled: 00:00:00.5460514
编译越来越慢的原因是因为每个创建的程序集都被添加为对所有后续脚本文件的引用。第一个文件可能有 10 个引用,第二个是 11 个,第三个是 12 个,依此类推。引用越多,编译时间越长。这就是下一行所做的,也是默认情况下 CS-Script 所做的。
parameters.ReferencedAssemblies.AddRange(AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic).Select(a => a.Location).ToArray());
不幸的是,我希望脚本能够相互引用,所以我现在必须想出一个引用系统。但至少我知道问题出在哪里。也许我会尝试使其自动化,记住在哪个程序集中可以找到哪些类型,以便自动引用它们。
确实,这可能是导致该行为的原因。事实上很有可能是。
您可以通过将 CSScript.ShareHostRefAssemblies 设置为 true 轻松更改行为。
不过在这种情况下,您需要注意所有引用其他脚本的脚本。一种方法是为所有 'shareable' 脚本提供所需的程序集文件名(在 Load(string scriptFile, string assemblyFile...)
中,然后将这些文件名用作您加载的所有脚本的输入。
//Assembly Load(string scriptFile, string assemblyFile, bool debugBuild, params string[] refAssemblies)
CSScript.Load("common_scriptA.cs", "asmA.dll", false);
CSScript.Load("common_scriptB.cs", "asmB.dll", false);
CSScript.Load("common_scriptC.cs", "asmC.dll", false);
CSScript.Load("normal_scriptA.cs", null, false, "asmC.dll", "asmB.dll", "asmC.dll");
CSScript.Load("normal_scriptB.cs", null, false, "asmC.dll", "asmB.dll", "asmC.dll");
我有一个使用 "scripts" 用 C# 编写的应用程序,在启动时使用 C# CodeDomProvider 编译。加载几十个,甚至超过 170 个脚本都不是什么大问题,我编译它们然后将程序集保存到缓存文件夹中以备下次启动时使用。对于 170 个脚本,此初始编译大约需要 1.5 分钟。
但是,当我尝试加载超过 1000 个脚本时,编译它们花了一个多小时。我添加了一个秒表并了解到每个脚本的加载时间都比之前的长一点,在 170 个文件的情况下,它从第一个文件的 ~150ms 到最后一个文件的 >650ms,每个文件都在一点点增加文件。
我知道我可以通过将脚本合并到一个大文件中来大大减少加载时间,但是出于多种原因,如果我可以单独编译它们,我会更喜欢它:/它使重新加载它们变得容易并且快,我不用担心当有什么变化时重新编译整个脚本文件夹,我可以很容易地给出一个编译进度的进度条等等
现在我的问题是,这里有什么问题?为什么每个文件的编译时间会随着时间的推移而增加?我可以做些什么吗?
编辑
我会尝试提供更多信息,如评论中的要求。
正如我所说,我正在使用 C# CodeDomProvider 编译每个脚本,基本上是循环中的以下内容,对于我当前的 171 个脚本文件中的每一个,每个文件包含一个或多个 类我在创建程序集后实例化:
var provider = System.CodeDom.Compiler.CodeDomProvider.CreateProvider("CSharp"); ;
var parameters = new System.CodeDom.Compiler.CompilerParameters();
parameters.ReferencedAssemblies.AddRange(AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic).Select(a => a.Location).ToArray());
parameters.GenerateExecutable = false;
parameters.GenerateInMemory = false;
parameters.OutputAssembly = tmpFileName + ".compiled";
parameters.TreatWarningsAsErrors = false;
parameters.WarningLevel = 0;
var results = provider.CompileAssemblyFromFile(parameters, tmpFileName);
asm = results.CompiledAssembly;
我使用 CS-Script 得到了相同的结果,它做了类似的事情。
asm = CSScript.LoadWithConfig(tmpFileName, null, debug, CSScript.GlobalSettings, "/warnaserror- /warn:0");
然后将创建的程序集保存到缓存文件夹中。当脚本文件比程序集旧时使用保存的程序集,所以我不必再次编译它。
当你坐在它前面时,它会随着时间的推移变得越来越慢,看着进度条越来越慢,所以我在上面的代码周围添加了一个简单的秒表调用,StartNew, Stop, Elapsed
,没有什么花哨。结果反映了我在进度条上看到的情况,每增加一个文件,编译时间就会增加。
timer.Restart();
// compile as shown above
timer.Stop();
Console.WriteLine(asm.Location + ": " + timer.Elapsed);
当然,文件之间会存在细微差异,具体取决于它们的大小和复杂程度,但脚本通常在大小和复杂性方面都或多或少相同,我正在目睹随着时间的推移持续增加。
Temp\tmpDDD7.tmp.compiled: 00:00:00.1389177
Temp\tmpDE74.tmp.compiled: 00:00:00.1327150
...
Temp\tmpE156.tmp.compiled: 00:00:00.1719746
Temp\tmpE213.tmp.compiled: 00:00:00.1431011
...
Temp\tmpF05C.tmp.compiled: 00:00:00.1696297
Temp\tmpF118.tmp.compiled: 00:00:00.1739564
...
Temp\tmpF7D5.tmp.compiled: 00:00:00.1824292
Temp\tmpF891.tmp.compiled: 00:00:00.1819889
...
Temp\tmp29F1.tmp.compiled: 00:00:00.2912163
Temp\tmp2B2B.tmp.compiled: 00:00:00.2909096
...
Temp\tmp362F.tmp.compiled: 00:00:00.3161408
Temp\tmp3773.tmp.compiled: 00:00:00.3170768
...
Temp\tmpA4C9.tmp.compiled: 00:00:00.5457990
Temp\tmpA6FC.tmp.compiled: 00:00:00.5460514
编译越来越慢的原因是因为每个创建的程序集都被添加为对所有后续脚本文件的引用。第一个文件可能有 10 个引用,第二个是 11 个,第三个是 12 个,依此类推。引用越多,编译时间越长。这就是下一行所做的,也是默认情况下 CS-Script 所做的。
parameters.ReferencedAssemblies.AddRange(AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic).Select(a => a.Location).ToArray());
不幸的是,我希望脚本能够相互引用,所以我现在必须想出一个引用系统。但至少我知道问题出在哪里。也许我会尝试使其自动化,记住在哪个程序集中可以找到哪些类型,以便自动引用它们。
确实,这可能是导致该行为的原因。事实上很有可能是。
您可以通过将 CSScript.ShareHostRefAssemblies 设置为 true 轻松更改行为。
不过在这种情况下,您需要注意所有引用其他脚本的脚本。一种方法是为所有 'shareable' 脚本提供所需的程序集文件名(在 Load(string scriptFile, string assemblyFile...)
中,然后将这些文件名用作您加载的所有脚本的输入。
//Assembly Load(string scriptFile, string assemblyFile, bool debugBuild, params string[] refAssemblies)
CSScript.Load("common_scriptA.cs", "asmA.dll", false);
CSScript.Load("common_scriptB.cs", "asmB.dll", false);
CSScript.Load("common_scriptC.cs", "asmC.dll", false);
CSScript.Load("normal_scriptA.cs", null, false, "asmC.dll", "asmB.dll", "asmC.dll");
CSScript.Load("normal_scriptB.cs", null, false, "asmC.dll", "asmB.dll", "asmC.dll");