Run
sudo sh -c 'sync ; echo 3 > /proc/sys/vm/drop_caches ; sync'
在你的开发机器上。这是确保缓存为空的安全、非破坏性方法。 (你会not即使您恰好同时保存或写入磁盘,运行上述命令也不会丢失任何数据。确实很安全。)
然后,确保没有运行任何 Java 内容,并重新运行上述命令以确保确定。例如,您可以检查是否有 Java 正在运行
ps axu | sed -ne '/ sed -ne /d; /java/p'
它不应该输出任何内容。如果是这样,请先关闭 Java 内容。
现在,重新运行您的应用程序测试。您的开发机器上现在是否也出现同样的速度下降情况?
如果您愿意以任何方式留下评论,德米特里,我很乐意进一步探讨这个问题。
编辑补充:我怀疑确实发生了速度减慢的情况,这是由于 Java 本身产生的较大启动延迟造成的。这是一个非常常见的问题,并且基本上是 Java 内置的,这是其架构的结果。对于较大的应用程序,无论机器的速度有多快,启动延迟通常都是一秒的相当大的一部分,这仅仅是因为 Java 必须加载和准备类(大多数情况下也是串行的,因此添加内核无济于事)。
换句话说,我认为这应该归咎于Java,而不是Linux;恰恰相反,因为 Linux 设法通过内核级缓存来减轻开发机器上的延迟,而这只是因为您实际上一直在运行这些 Java 组件,因此内核知道缓存它们。
编辑 2:当应用程序启动时,查看 Java 环境访问哪些文件将非常有用。你可以这样做strace
:
strace -f -o trace.log -q -tt -T -e trace=open COMMAND...
它创建文件trace.log
含有open()
由以下启动的任何进程完成的系统调用COMMAND...
。将输出保存到trace.PID
对于每个进程COMMAND...
启动,使用
strace -f -o trace -ff -q -tt -T -e trace=open COMMAND...
比较开发和生产安装上的输出将告诉您它们是否真正相同。其中之一可能有额外或缺失的库,从而影响启动时间。
如果安装较旧且系统分区相当满,则这些文件可能已形成碎片,导致内核花费更多时间等待 I/O 完成。 (请注意,amountI/O 保持不变;只是如果文件碎片化,完成所需的时间会增加。)您可以使用命令
LANG=C LC_ALL=C sed -ne 's|^[^"]* open("\(.*\)", O[^"]*$|\1|p' trace.* \
| LANG=C LC_ALL=C sed -ne 's|^[^"]* open("\(.*\)", O[^"]*$|\1|p' \
| LANG=C LC_ALL=C xargs -r -d '\n' filefrag \
| LANG=C LC_ALL=C awk '(NF > 3 && $NF == "found") { n[$(NF-2)]++ }
END { for (i in n) printf "%d extents %d files\n", i, n[i] }' \
| sort -g
检查应用程序使用的文件的碎片程度;它报告有多少文件仅使用一个或多个扩展区。请注意,它不包括原始可执行文件(COMMAND...
),仅它访问的文件。
如果您只想获取单个命令访问的文件的碎片统计信息,您可以使用
LANG=C LC_ALL=C strace -f -q -tt -T -e trace=open COMMAND... 2>&1 \
| LANG=C LC_ALL=C sed -ne 's|^[0-9:.]* open("\(.*\)", O[^"]*$|\1|p' \
| LANG=C LC_ALL=C xargs -r filefrag \
| LANG=C LC_ALL=C awk '(NF > 3 && $NF == "found") { n[$(NF-2)]++ }
END { for (i in n) printf "%d extents %d files\n", i, n[i] }' \
| sort -g
如果问题不是由于缓存引起的,那么我认为这两种安装很可能并不真正等效。如果是,那么我会检查碎片。之后,我会进行完整的跟踪(省略-e trace=open
)在两种环境上查看到底差异在哪里。
我相信我现在了解您的问题/情况。
在您的产品环境中,内核页面缓存大部分是脏的,即大多数缓存的内容都是将要写入磁盘的内容。
当您的应用程序分配新页面时,内核仅设置页面映射,实际上并不会立即提供物理RAM。这只发生在第一次访问每个页面时。
在第一次访问时,内核首先找到一个空闲页面——通常是一个包含“干净”缓存数据的页面,即从磁盘读取但未修改的数据。然后,它将其清零,以避免进程之间的信息泄漏。 (当使用 C 库分配工具时,例如malloc()
等等,而不是直接mmap()
函数族,库可以使用/重用映射的部分。尽管内核确实将页面清零,但库可能会“弄脏”它们。使用mmap()
要获得匿名页面,您必须将它们归零。)
如果内核手头没有合适的干净页,它必须首先将一些最旧的脏页刷新到磁盘。 (内核内部有一些进程将页面刷新到磁盘,并将它们标记为干净,但是如果服务器负载导致页面不断变脏,通常希望拥有大部分脏页面而不是大部分干净页面 - 服务器会得到这样可以完成更多的工作。不幸的是,这也意味着首页访问延迟的增加,您现在遇到了这种情况。)
每一页都是sysconf(_SC_PAGESIZE)
字节长,对齐。换句话说,指针p
指向页面的开头当且仅当((long)p % sysconf(_SC_PAGESIZE)) == 0
。我相信,大多数内核在大多数情况下实际上都会填充页面组而不是单个页面,从而增加了首次访问(对每组页面)的延迟。
最后,可能有一些编译器优化会对您的基准测试造成严重破坏。我建议您为基准测试编写一个单独的源文件main()
,以及每次迭代完成的实际工作在单独的文件中。单独编译它们,然后将它们链接在一起,以确保编译器不会重新排列时间函数。实际完成的工作。基本上,在benchmark.c
:
#define _POSIX_C_SOURCE 200809L
#include <time.h>
#include <stdio.h>
/* in work.c, adjust as needed */
void work_init(void); /* Optional, allocations etc. */
void work(long iteration); /* Completely up to you, including parameters */
void work_done(void); /* Optional, deallocations etc. */
#define PRIMING 0
#define REPEATS 100
int main(void)
{
double wall_seconds[REPEATS];
struct timespec wall_start, wall_stop;
long iteration;
work_init();
/* Priming: do you want caches hot? */
for (iteration = 0L; iteration < PRIMING; iteration++)
work(iteration);
/* Timed iterations */
for (iteration = 0L; iteration < REPEATS; iteration++) {
clock_gettime(CLOCK_REALTIME, &wall_start);
work(iteration);
clock_gettime(CLOCK_REALTIME, &wall_stop);
wall_seconds[iteration] = (double)(wall_stop.tv_sec - wall_start.tv_sec)
+ (double)(wall_stop.tv_nsec - wall_start.tv_nsec) / 1000000000.0;
}
work_done();
/* TODO: wall_seconds[0] is the first iteration.
* Comparing to successive iterations (assuming REPEATS > 0)
* tells you about the initial latency.
*/
/* TODO: Sort wall_seconds, for easier statistics.
* Most reliable value is the median, with half of the
* values larger and half smaller.
* Personally, I like to discard first and last 15.85%
* of the results, to get "one-sigma confidence" interval.
*/
return 0;
}
实际的数组分配、释放和填充(每个重复循环)在work()
函数定义在work.c
.