我有一个很大的MATLAB函数文件。它首先创建一个零矩阵,然后通过计算函数中硬编码的许多相应的(长)代数表达式来更新大约 70% 的单元格。完成此操作后,将返回一个数字矩阵。
.m文件大约4 MB大(我有100个这样的m.文件,但这并不直接相关)。当我第一次计算函数时,大约需要 9 秒来评估。然而,随后的运行只需要大约 0.1 秒,这超出了我的预期。
为什么第一次评估需要 9 秒?每当我关闭并重新打开 MATLAB 时,每次我都会进行这种缓慢的第一次评估,随后的运行要快得多。这是为什么呢?
m. 文件可以在下面的公共链接中找到(您可以从浏览器中复制文本):https://dl.dropboxusercontent.com/u/157153767/K_a_12_102x.m
应使用的命令窗口输入为:[test]=K_a_12_102x(414000000,1.1095e+09,1.2500e-04,0.0840,0.0840,0.0240,0.0240,0.0020,0.0020,0,0,0,0,3.0397e+08,8.9930e+07,0,3.0397e+08,0,1.0702e+08,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,000,0,0,0,000,0,000,0,0,000,0,000,0,00,0,0,000,0,000,0,000,0,0,00,000,0,00,0,0,00,0,00,0,0,0,0,0,00,0,0,00,0,000,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,00,0,00,0,00,0000,0,0000,0,00,0000,0,0000,0,0000,0,0000,0000,0,0000,0,0000,0,0000,000,0000,0.06,
早在 JIT 编译器引入 MATLAB 之前,首次运行速度慢就已经存在,甚至对于未应用 JIT 编译器的 MEX 文件也是如此。首次运行代码时,MATLAB 必须从磁盘加载代码,解析代码(请参阅下面的运行时类型分析详细信息),如果它是 .m 文件,则应用 JIT 编译。 然后在执行时,分配数据空间,并将指令加载到 CPU 缓存中,在那里它们可能会保持非常快的访问时间以进行进一步执行。 据我所知,这就是 MATLAB 世界之外无处不在的"缓存预热"程序的原因(为我的挥手向硬件爱好者道歉)。 但是,对于 .m fies,磁盘访问可能是一个重要因素,即使文件比"大约 4MB 大"小得多,就像您的情况一样。 当多个函数具有相同的名称时,还有一个额外的函数消除歧义步骤。
要查看 MEX 文件发生这种情况,只需运行 clear mex
并对函数调用进行计时。 MATLAB 必须将其从磁盘重新加载到内存中,可能是使用无效的 CPU 缓存。
运行时类型分析
代码加速功能的第二个方面(JIT 代码生成是第一个方面)是运行时类型分析。 来自一份旧的MathWork白皮书:
运行时类型分析基于以下前提:如果以前处理过一行 M 代码,则变量很可能具有与系统上次看到此行时相同的类型和形状。首次执行一行代码时,系统会检查变量并为找到的数据类型和形状生成特定代码。只要系统验证变量类型和大小未更改,该行的后续执行就可以重用此代码。由于类型很少更改,因此后续执行会尽快运行。如果类型确实发生更改,则会重新生成代码。
您可能会考虑 JIT 编译过程的这一部分,事实确实如此。 但关键是,无论加速器是否决定对任何代码行进行 JIT 编译,此分析都会在首次执行时运行。 顺便说一句,整个文件不会被编译成机器代码。过去可以使用setpref('profiler','showJitLines',1);
在探查器中查看哪些行被加速,但不幸的是,这作为一项功能被删除了。
无论如何,在实际查看代码后,在从磁盘加载文件后需要解析数量惊人的常量和变量。 一行长度超过 31,000 个字符,有数千个数字文字! 分析和决定需要编译的内容以及运行之间可以缓存的内容非常重要。 好像是为了证明这一点,仅查看(未运行)您的代码就设法使编辑器崩溃,堆栈跟踪DirectUI::DUIXmlParser::InitializeParserFromXmlLiteReader
。哎呀,这是一些讨厌的代码!
JIT 编译器是否为此函数生成代码?
让我们在打开 MATLAB 加速功能的情况下对代码进行计时。 我们还运行控制测试,我们知道在没有加速的情况下运行速度慢约 8 倍。
>> feature accel on
>> clear K_a_12_102x
>> x = rand(1000); tic,for t=1:1e6, x=x; end,toc % control
Elapsed time is 0.083878 seconds.
% do first-run of K_a_12_102x, took 13.280327 sec
>> tic; [test]=K_a_12_102x(414000000,1.1095e+09,1.2500e-04,0.0840,0.0840,0.0240,0.0240,0.0020,0.0020,0,0,0,0,3.0397e+08,8.9930e+07,0,3.0397e+08,0,1.0702e+08,0,0,0,0,0,0,497.7389,80.7355,-15.9811,391.1985,-15.9811,103.5248,20440000,0,20440000,0.06); toc
Elapsed time is 0.151804 seconds.
现在我们关闭加速并运行相同的测试:
>> feature accel off
>> clear K_a_12_102x
>> tic,for t=1:1e6, x=x; end,toc % control
Elapsed time is 0.630039 seconds.
% do a first-run of K_a_12_102x, took 15.634775 seconds
>> tic; [test]=K_a_12_102x(414000000,1.1095e+09,1.2500e-04,0.0840,0.0840,0.0240,0.0240,0.0020,0.0020,0,0,0,0,3.0397e+08,8.9930e+07,0,3.0397e+08,0,1.0702e+08,0,0,0,0,0,0,497.7389,80.7355,-15.9811,391.1985,-15.9811,103.5248,20440000,0,20440000,0.06); toc
Elapsed time is 0.159683 seconds.
发现
调查结果有两个方面:
- 禁用加速 (JIT) 后,首次运行时间不会缩短(JIT 打开时为 13.28 秒,关闭 JIT 时为 15.63 秒)。
- 随后的运行表明,启用 JIT 时不会生成任何机器代码(JIT 打开时为 0.1518 秒,JIT 关闭时为 0.1597 秒)
简而言之,您的代码不会从 JIT 加速中受益,并且 JIT 执行/分析不会增加首次运行执行时间。
问题仍然存在,是什么原因导致首次运行时间缓慢?一些可能性:从磁盘加载代码文本,在将代码保存在 RAM 中之前解析(去除注释和空格),不重用以前运行中保存的变量初始化,可能是保存在 CPU 缓存中的函数使用的核心 MATLAB 指令,以及 MATLAB 进行运行时语法检查所需的任何非 JIT 相关代码分析。 该文件是 4MB 并且在方程长度和数字文字的绝对数量方面非常复杂,这一事实表明它不是 CPU 缓存,而是初始文件加载和代码分析。
我认为这就是 JIT 编译。第一次执行文件时,MATLAB 必须对其进行解释(将文本转换为机器代码)。
后续运行使用缓存的计算机代码,执行速度要快得多。要验证这一点:在代码进行小幅更改后,MATLAB 将需要重做此编译 - 因此下次再次运行应该很慢。(我这样做正是出于这个后果。您正在执行大量简单的操作,这些操作应该执行得非常快。转换为机器代码会减慢您的速度。
为了加快整个过程:将代码传输到 C、C# 或类似的东西,并将其作为 DLL 文件包含在内。您将拥有恒定而快速的计算,但您无法轻松更改它们。
(使用 C# DLL 文件,您也有一些 JIT 编译,但它的频率较低,可能仍然比 MATLAB 快。
我已经做了一些编码并将代码移植到 C#。原始计时为 13.4 秒和 0.15 秒(第一次/第二次运行)
简单端口,发布配置:
>> test_1
Elapsed time is 124.7 seconds.
>> test_1
Elapsed time is 0.0297 seconds.
所以第一次运行比 MATLAB 差得多 - 无赖。虽然我喜欢 C#,但它可能不是这项工作的最佳工具。(顺便说一句:我不得不使用命令行编译器,因为Visual Studio 2010不断崩溃......)
除了TheCrumbMonster提到的JIT编译器的效果之外,可能还有各种缓存效果。要么 Matlab 本身足够聪明,可以重用它的一些数据结构,要么它的一些代码已经存储在处理器的缓存中,而不是在主内存中。事实上,即使是 JIT 本身也依赖于缓存编译结果,否则每次调用函数时都必须重新编译。此外,所有现代操作系统都执行各种缓存,因此您无需从磁盘读取某些数据文件,MEX文件或DLL,只需从内存中获取即可。
这就是为什么为了准确测量某些函数的执行速度,您不应该使用简单的tic(); toc()
语句,而应该使用像timeit这样的函数(使用它,它非常好!这会多次重复测量以"预热"缓存,并始终丢弃前几个测量值。
至于 matlab 对于这个特定文件很慢的原因,我完全可以理解。我的文本编辑器需要一分多钟才能打开它,而您大约有 100 行带有极长的表单语句
K_a_12=zeros(1089,100);
K_a_12(1011,1) = 2*h_a*((x*(250*G_a*L2^20*W + 5250*G_a*L1^2*L2^18*W + ...
K_a_12(1011,3) = 2*h_a*((x^13*(188955000*G_a*L2^8*W*h_1 + ...
哪些(我希望)是自动生成的。在我看来,计算可以更有效地完成。首先,您似乎只填充了矩阵的一小部分,因此您可能应该使用稀疏矩阵。接下来,从快速检查来看,每个项似乎都是h_a * x^n1 * const * L1^n2 * L2^n3 * ....
形式,可以用简化的方式计算。我敢打赌,整个计算可以通过几个矩阵的乘法和幂在几行内完成,这些矩阵应该保存为mat文件,而不是像完整写出的计算那样。最后,您也不要使用函数的大约一半的输入参数。