我的经过时间为什么比用户时间长得多?

10

我正在对一些R语句进行基准测试(详见此处),发现我的经过时间比用户时间要长得多。

   user  system elapsed 
  7.910   7.750  53.916 

有人能帮助我理解是什么因素(R或Hardware)决定了用户时间和经过时间之间的差异,以及如何改善它吗?如果可以提供帮助的话,我正在Macbook Air 1.7Ghz i5,4GB RAM上运行data.table数据操作。

更新:我粗略的理解是,用户时间是CPU处理我的作业所需的时间。经过时间是从我提交作业到获取数据返回的长度。在处理8秒后,我的计算机还需要做什么?

更新:如评论中建议的那样,我对两个data.table进行了几次运行:Y具有104列(抱歉,我添加了更多列),而X是仅具有3个键的Y的子集。以下是更新内容。请注意,我连续运行了这两个过程,因此内存状态应该是相似的。

 X<- Y[, list(Year, MemberID, Month)]

 system.time(
   {X[ , Month:= -Month]
   setkey(X,Year, MemberID, Month)
   X[,Month:=-Month]}
  )
   user  system elapsed 
  3.490   0.031   3.519 

 system.time(
 {Y[ , Month:= -Month]
  setkey(Y,Year, MemberID, Month)
  Y[,Month:=-Month]}
 )
   user  system elapsed 
  8.444   5.564  36.284 

这是我工作空间中唯一两个对象的大小(已添加逗号):

object.size(X)
83,237,624 bytes

 object.size(Y)
2,449,521,080 bytes

谢谢你


3
请参考?system.time中的建议,查看?proc.time。你需要澄清对那里所写内容有什么疑惑? - Joshua Ulrich
1
我的粗略理解是用户时间是CPU处理作业所需的时间。经过的时间是从我提交作业到收到数据的长度。在处理了8秒钟后,我的计算机还需要做什么? - AdamNYC
3
一个可重现的示例将有很大帮助。 - GSee
3
这不取决于您的计算机上运行的其他进程吗?也许这是一个愚蠢的问题,但在测试时间时,确保退出其他应用程序可能是个好主意。 - joran
2
当您看到这种行为时,您的交换使用情况是多少?拥有4G的RAM和一个接近2.5G的单个对象,我会预计会有一些争用。您可能只需要更多的RAM。 - Matthew Lundberg
显示剩余5条评论
1个回答

23

用户时间是计算机花费在执行您的计算的秒数。系统时间是操作系统响应程序请求所花费的时间。经过时间是这两个时间加上您的程序和/或操作系统必须等待的任何“等待”时间的总和。需要注意的是,这些数字是花费的累积时间。您的程序可能会计算1秒钟,然后在操作系统上等待1秒钟,然后在硬盘上等待3秒钟,而当它运行时可能会重复这个周期多次。

根据您的程序所花费的系统时间与用户时间相同的事实,说明它需要大量进行IO操作。频繁地从磁盘读取或向磁盘写入数据。RAM速度很快,通常只需要几百纳秒。因此,如果所有内容都适合RAM,经过的时间通常比用户时间稍长一点。但是,磁盘可能需要几毫秒来搜索,甚至更长时间才能回复数据。这比RAM慢了一百万倍。

我们已确定您的处理器正在执行某些任务约为8 + 8 = ~16秒。那其他的38秒呢?在等待硬盘将其请求的数据发送给它。

更新1:

Matthew提出了一些很好的观点,我做出了一些可能不应该做出的假设。如果Adam愿意发布表中的所有行(我们只需要数据类型),我们可以更好地了解情况。

我刚刚编写了一个小程序来验证我的假设:未在用户空间和内核空间消耗时间的时间很可能是用于等待IO操作。

#include <stdio.h>
int main()
{
    int i;
    for(i = 0; i < 1000000000; i++)
    {
        int j, k, l, m;
        j = 10;
        k = i;
        l = j + k;
        m = j + k - i + l;
    }
    return 0;
}
当我运行生成的程序并计时时,我会看到类似于这样的东西:
mike@computer:~$ time ./waste_user
real    0m4.670s
user    0m4.660s
sys 0m0.000s
mike@computer:~$ 

通过检查,您可以看到该程序没有进行任何真正的工作,因此除了将其加载到RAM并开始运行之外,它不会要求内核做任何事情。 因此,几乎所有的“真实”时间都花费在“用户”时间上。

现在,一个依赖于内核的空操作程序(少了一些迭代以使时间合理):

#include <stdio.h>
int main()
{
    FILE * random;
    random = fopen("/dev/urandom", "r");
    int i;
    for(i = 0; i < 10000000; i++)
    {
        fgetc(random);
    }
    return 0;
}
当我运行那个时,我会看到更像这样的东西:
mike@computer:~$ time ./waste_sys
real    0m1.138s
user    0m0.090s
sys     0m1.040s
mike@computer:~$ 

可以通过检查轻松地看出程序只是请求内核给它随机字节。 /dev/urandom是一种非阻塞的熵源。那是什么意思?内核使用伪随机数生成器快速为我们的小测试程序生成“随机”值。这意味着内核必须进行一些计算,但它可以很快返回。因此,该程序大部分时间都在等待内核为其计算,我们可以看到几乎所有时间都花费在sys上。

现在,我们要做出一个小改变。我们将不再从非阻塞的/dev/urandom读取,而是从阻塞的/dev/random读取。那是什么意思?它不进行太多计算,而是等待你的计算机上发生内核开发人员经验确定为随机的事情。(我们还会进行更少的迭代,因为这需要更长时间)

#include <stdio.h>
int main()
{
    FILE * random;
    random = fopen("/dev/random", "r");
    int i;
    for(i = 0; i < 100; i++)
    {
        fgetc(random);
    }
    return 0;
}

当我运行并计时程序的这个版本时,我看到的是:

mike@computer:~$ time ./waste_io
real    0m41.451s
user    0m0.000s
sys     0m0.000s
mike@computer:~$ 

运行时花费了41秒,但在用户和实际时间上却只有微不足道的少量时间。为什么呢?所有时间都花在内核中了,但没有进行主动计算,内核只是在等待事件发生。一旦收集到足够的熵,内核就会重新唤醒并将数据发送回程序。(请注意,根据正在进行的情况,它可能需要更少或更多的时间才能在您的计算机上运行)。我认为用户+系统时间与实际时间之间的时间差是由于IO操作。

那么这一切意味着什么呢?这并不能证明我的答案是正确的,因为你看到这种行为还可能有其他解释。但它确实展示了用户计算时间、内核计算时间以及我声称用于执行IO操作的时间之间的区别。

这是我关于/dev/urandom和/dev/random之间差异的来源:http://en.wikipedia.org/wiki//dev/random

更新2:

我想尝试解决Matthew建议的问题的根源可能是L2缓存丢失。Core i7具有64字节的缓存行。我不知道你对高速缓存了解多少,所以我提供一些详细信息。当您从内存中请求一个值时,CPU不仅获取那个值,还会获取其周围的所有64个字节。这意味着如果您以非常可预测的模式访问内存,比如说array[0]、array[1]、array[2]等,获取值0需要一段时间,但之后的1、2、3、4..就会快很多。直到你到了下一个缓存行为止。如果这是int数组,0将会很慢,1..15将会很快,16将会很慢,17..31将会很快,依此类推。

http://software.intel.com/en-us/forums/topic/296674

为了测试这个,我做了两个程序。它们都有一个包含1024*1024个元素的结构体数组。在一个案例中,结构体中有一个双精度浮点数,在另一个案例中它有8个双精度浮点数。一个双精度浮点数长8字节,所以在第二个程序中,我们以最糟糕的方式访问内存。第一个程序将能够很好地使用缓存。

#include <stdio.h>
#include <stdlib.h>
#define MANY_MEGS 1048576
typedef struct {
    double a;
} PartialLine;
int main()
{
    int i, j;
    PartialLine* many_lines;
    int total_bytes = MANY_MEGS * sizeof(PartialLine);
    printf("Striding through %d total bytes, %d bytes at a time\n", total_bytes, sizeof(PartialLine));
    many_lines = (PartialLine*) malloc(total_bytes);
    PartialLine line;
    double x;
    for(i = 0; i < 300; i++)
    {
        for(j = 0; j < MANY_MEGS; j++)
        {
            line = many_lines[j];
            x = line.a;
        }
    }
    return 0;
}

运行此程序时,我看到以下输出:

mike@computer:~$ time ./cache_hits
Striding through 8388608 total bytes, 8 bytes at a time
real    0m3.194s
user    0m3.140s
sys     0m0.016s
mike@computer:~$

这是拥有大型结构体的程序,它们每个占用64字节的内存,而不是8字节。

#include <stdio.h>
#include <stdlib.h>
#define MANY_MEGS 1048576
typedef struct {
    double a, b, c, d, e, f, g, h;
} WholeLine;
int main()
{
    int i, j;
    WholeLine* many_lines;
    int total_bytes = MANY_MEGS * sizeof(WholeLine);
    printf("Striding through %d total bytes, %d bytes at a time\n", total_bytes, sizeof(WholeLine));
    many_lines = (WholeLine*) malloc(total_bytes);
    WholeLine line;
    double x;
    for(i = 0; i < 300; i++)
    {
        for(j = 0; j < MANY_MEGS; j++)
        {
            line = many_lines[j];
            x = line.a;
        }
    }
    return 0;
}

当我运行它时,我看到这个:

mike@computer:~$ time ./cache_misses
Striding through 67108864 total bytes, 64 bytes at a time
real    0m14.367s
user    0m14.245s
sys     0m0.088s
mike@computer:~$ 

第二个程序——那个特意设计成缓存未命中的程序——要运行相同数量的内存访问,需要五倍的时间。

值得注意的是,在两种情况下,所有耗费的时间都花在了用户空间而不是系统空间。这意味着操作系统将程序等待数据的时间计入程序的时间,而不是计入操作系统的时间。根据这两个例子,我认为缓存未命中不太可能导致你的流逝时间比你的用户时间长得多。

更新3:

我刚看到你的更新,说压缩后的表比常规大小的表运行快了10倍左右。这也说明了(正如另一个Matthew所说的那样),你的电脑已经用完了RAM。

一旦你的程序尝试使用比你的计算机实际安装的更多的内存,它就开始交换到磁盘。这比你的程序崩溃要好,但它比RAM慢得多,可能会导致大幅减速。

明天我会尝试提供一个显示交换问题的示例。

更新4:

好的,这里有一个非常类似于之前程序的示例程序。但现在结构体的大小是4096字节,而不是8字节。总共,这个程序将使用2GB的内存,而不是64MB。我还稍微改变了一下,确保随机访问而不是逐个元素访问,这样内核就不会聪明地开始预测我的程序需要什么。缓存由硬件驱动(仅由简单的启发式算法驱动),但kswapd(内核交换守护进程)完全可能比缓存聪明得多。

#include <stdio.h>
#include <stdlib.h>
typedef struct {
    double numbers[512];
} WholePage;
int main()
{
    int memory_ops = 1024*1024;
    int total_memory = memory_ops / 2;
    int num_chunks = 8;
    int chunk_bytes = total_memory / num_chunks * sizeof(WholePage);
    int i, j, k, l;
    printf("Bouncing through %u MB, %d bytes at a time\n", chunk_bytes/1024*num_chunks/1024, sizeof(WholePage));
    WholePage* many_pages[num_chunks];
    for(i = 0; i < num_chunks; i++)
    {
        many_pages[i] = (WholePage*) malloc(chunk_bytes);
        if(many_pages[i] == 0){ exit(1); }
    }
    WholePage* page_list;
    WholePage* page;
    double x;
    for(i = 0; i < 300*memory_ops; i++)
    {
        j = rand() % num_chunks;
        k = rand() % (total_memory / num_chunks);
        l = rand() % 512;
        page_list = many_pages[j];
        page = page_list + k;
        x = page->numbers[l];
    }
    return 0;
}

从我调用的cache_hits到cache_misses程序中,我们看到内存大小增加了8倍,执行时间增加了5倍。当我们运行这个程序时,你会预期看到什么?它使用的内存量是cache_misses的32倍,但具有相同数量的内存访问。

mike@computer:~$ time ./page_misses
Bouncing through 2048 MB, 4096 bytes at a time
real    2m1.327s
user    1m56.483s
sys     0m0.588s
mike@computer:~$ 

在拥有4GB内存的计算机上,它花费了cache_misses的8倍时间和cache_hits的40倍时间。我的程序使用了50%的内存,而cache_misses只使用1.5%,cache_hits只使用0.2%。即使没有使用完整个计算机的所有内存,它的运行速度也明显变慢,足以对程序性能产生显著影响。希望这对诊断程序运行缓慢问题有所帮助。


在这种情况下,你怎么能确定是磁盘I/O呢? - Matt Dowle
50字节?在R中,整数=4,数字=8,字符=4(32位)或8(64位),因为字符字符串被缓存。因此,最多3e6 * 40 * 8 / 1024^3=0.9GB +添加到R的全局缓存中的唯一字符串量(如果有)。如果我理解正确的话。 - Matt Dowle
马修,你抓住我了。可惜我已经摧毁了月球。哎呀! - Mike Sandford
嗨,Mike和Matthew:我从你们的讨论中学到了很多。Matthew: 我重新启动了电脑,只是用Firefox运行R,等待时间仍然很长。让我今天稍后再试一次并向你们更新。我也会加载变量类型。有没有办法确定是否为磁盘I/O进行测试? - AdamNYC
关键列有哪些类型?如果有任何字符列,请尝试转换为因子。这也可能是多列排序的方式[FR#2419 fastorder可以从左到右迭代](https://r-forge.r-project.org/tracker/index.php?func=detail&aid=2419&group_id=240&atid=978)。希望提供一些可重复的内容。 - Matt Dowle
显示剩余15条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接