我意识到这个话题早就死了,但想把我的发现扔进去。这是一个很长的答案,所以我把简短的答案放在这里,有耐心的人可以完成剩下的。这个问题的答案不完全是 700 ns 或 1500 ns,具体取决于您使用的 clock_gettime() 模式。长答案要复杂得多。
作为参考,我做这项工作的机器是一台没人想要的旧笔记本电脑。它是运行 Ubuntu 14.041 LTS 的 Acer Aspire 5720Z。
硬件:
RAM:2.0 GiB // 这是 Ubuntu 在“系统设置”→“详细信息”中报告的方式
处理器:Intel® Pentium(R) Dual CPU T2330 @ 1.60GHz × 2
图形:Intel® 965GM x86/MMX/SSE2
我想在即将到来的项目中准确测量时间,并且作为一个相对较新的 PC 硬件,不管操作系统如何,我想我会在计时硬件的分辨率上做一些实验。我偶然发现了这个问题。
由于这个问题,我认为clock_gettime() 看起来符合我的需求。但是我过去在 PC 硬件方面的经验让我感到不知所措,所以我重新开始做一些实验,看看计时器的实际分辨率是多少。
方法:从clock_gettime() 收集结果的连续样本,并查看分辨率中的任何模式。代码如下。
结果略长摘要:
- 并不是真正的结果。结构中场的规定分辨率以纳秒为单位。调用clock_getres() 的结果也是tv_sec 0,tv_nsec 1。但以前的经验告诉我们不要只相信结构的分辨率。这是精度的上限,而现实往往要复杂得多。
- 在我的机器上,我的程序,我的操作系统,在某一天等上,clock_gettime() 结果的实际分辨率对于模式 0 和 1 来说是 70 纳秒。70 ns 并不算太糟糕,但不幸的是,这是不现实,我们将在下一点看到。更复杂的是,使用模式 2 和 3 时的分辨率似乎为 7 ns。
- 对于模式 0 和 1,clock_gettime() 调用的持续时间更像是 1500 ns。如果需要 20 倍的分辨率才能获得一个值,那么在时间上要求 70 ns 的分辨率对我来说根本没有意义。
- clock_gettime() 的某些模式比其他模式更快。模式 2 和 3 显然是模式 0 和 1 的挂钟时间的一半左右。模式 0 和 1 在统计上彼此无法区分。模式 2 和 3 比模式 0 和 1 快得多,模式 3 总体上是最快的。
在继续之前,我最好定义一下模式:哪个模式是哪个?:
模式 0 CLOCK_REALTIME //参考:http
:
//linux.die.net/man/3/clock_gettime 模式 1 CLOCK_MONOTONIC
模式 2 CLOCK_PROCESS_CPUTIME_ID
模式 3 CLOCK_THREAD_CPUTIME_ID
结论:对我来说,如果分辨率小于函数获取时间间隔所需的时间长度,那么谈论时间间隔的分辨率是没有意义的。例如,如果我们使用模式 3,我们知道该函数在 99% 的时间内在 700 纳秒内完成。我们进一步知道,我们返回的时间间隔将是 7 纳秒的倍数。所以 7 纳秒的“分辨率”是调用时间的 1/100。我在 7 纳秒的更改间隔中看不到任何值。分辨率问题有 3 个不同的答案:1 ns、7 或 70 ns,最后是 700 或 1500 ns。我赞成最后一个数字。
毕竟,如果你想衡量某个操作的性能,你需要记住 clock_gettime() 调用需要多长时间——即 700 或 1500 ns。例如,尝试测量需要 7 纳秒的东西是没有意义的。为了争论,假设您愿意忍受性能测试结论的 1% 错误。如果使用模式 3(我想我将在我的项目中使用),您将不得不说您需要测量的时间间隔需要是 100 乘以 700 纳秒或 70 微秒。否则你的结论会有超过1%的错误。所以继续测量你感兴趣的代码,但是如果你在感兴趣的代码中经过的时间少于 70 微秒,
这些说法的理由和一些细节:
先声明3。这很简单。只需多次运行clock_gettime()并将结果记录在一个数组中,然后对结果进行处理。在循环之外进行处理,以使 clock_gettime() 调用之间的时间尽可能短。
这一切意味着什么?见附图。例如,对于模式 0,对 clock_gettime() 的调用在大多数情况下花费的时间不到 1.5 微秒。可以看到mode 0和mode 1基本一样。但是,模式 2 和 3 与模式 0 和 1 非常不同,并且彼此之间也略有不同。与模式 0 和 1 相比,模式 2 和 3 占用的时钟时间大约是时钟时间的一半。另请注意,模式 0 和 1 彼此略有不同 - 与模式 2 和 3 不同。请注意,模式 0 和 1 不同70 纳秒——这是我们将在权利要求 2 中提到的数字。
所附图表的范围限制为 2 微秒。否则,数据中的异常值会阻止图形传达前一点。图表没有说明的是,模式 0 和 1 的异常值比模式 2 和 3 的异常值差得多。换句话说,不仅平均值和统计“模式”(发生的值最大)和所有这些模式的中位数(即第 50 个百分位数)不同,因此最大值和它们的第 99 个百分位数也不同。
所附图表是四种模式中每一种模式的 100,001 个样本。请注意,绘制的测试仅使用处理器 0 的 CPU 掩码。我是否使用 CPU 亲和性似乎对图表没有任何影响。
主张 2:如果您在准备图表时仔细观察收集的样本,您很快就会注意到差异之间的差异(即二阶差异)是相对恒定的——大约为 70 纳秒(至少在模式 0 和 1 之前)。要重复此实验,请像以前一样收集“n”个时钟时间样本。然后计算每个样本之间的差异。现在将差异按顺序排序(例如 sort -g),然后得出个体的独特差异(例如 uniq -c)。
例如:
$ ./Exp03 -l 1001 -m 0 -k | sort -g | awk -f mergeTime2.awk | awk -f percentages.awk | sort -g
1.118e-06 8 8 0.8 0.8 // time,count,cumulative count, count%, cumulative count%
1.188e-06 17 25 1.7 2.5
1.257e-06 9 34 0.9 3.4
1.327e-06 570 604 57 60.4
1.397e-06 301 905 30.1 90.5
1.467e-06 53 958 5.3 95.8
1.537e-06 26 984 2.6 98.4
<snip>
第一列中的持续时间之间的差异通常是 7e-8 或 70 纳秒。这可以通过处理差异变得更加清晰:
$ <as above> | awk -f differences.awk
7e-08
6.9e-08
7e-08
7e-08
7e-08
7e-08
6.9e-08
7e-08
2.1e-07 // 3 lots of 7e-08
<snip>
请注意所有差异都是 70 纳秒的整数倍?或者至少在 70 纳秒的舍入误差内。
这个结果很可能取决于硬件,但我实际上不知道此时将其限制为 70 纳秒的原因。也许某处有 14.28 MHz 振荡器?
请注意,在实践中,我使用了更多的样本,例如 100,000,而不是上面的 1000。
相关代码(附):
'Expo03' 是尽可能快地调用clock_gettime() 的程序。请注意,典型用法类似于:
./Expo03 -l 100001 -m 3
这将调用 clock_gettime() 100,001 次,以便我们可以计算 100,000 个差异。本例中对 clock_gettime() 的每次调用都将使用模式 3。
MergeTime2.awk 是一个有用的命令,它是一个美化的“uniq”命令。问题是二阶差异通常是成对的 69 和 1 纳秒,而不是 70(至少对于模式 0 和 1),因为我让你相信到目前为止。因为没有 68 纳秒或 2 纳秒的差异,所以我将这 69 和 1 纳秒对合并为一个 70 纳秒的数字。为什么会发生 69/1 行为很有趣,但是将它们视为两个独立的数字通常会在分析中增加“噪音”。
在你问之前,我已经重复了这个避免浮点的练习,但同样的问题仍然存在。结果 tv_nsec 作为整数具有这种 69/1 行为(或 1/7 和 1/6),因此请不要假设这是由浮点减法引起的伪影。
请注意,我对 70 ns 和 70 ns 的小整数倍的这种“简化”很有信心,但这种方法在 7 ns 的情况下看起来不太稳健,尤其是当您获得 10 倍于 7 ns 分辨率的二阶差异时。
percents.awk 和 Difference.awk 以防万一。
停止新闻:我无法发布图表,因为我没有“至少 10 的声誉”。对不起。
罗布·沃森 2014 年 11 月 21 日
Expo03.cpp
/* Like Exp02.cpp except that here I am experimenting with
modes other than CLOCK_REALTIME
RW 20 Nov 2014
*/
/* Added CPU affinity to see if that had any bearing on the results
RW 21 Nov 2014
*/
#include <iostream>
using namespace std;
#include <iomanip>
#include <stdlib.h> // getopts needs both of these
#include <unistd.h>
#include <errno.h> // errno
#include <string.h> // strerror()
#include <assert.h>
// #define MODE CLOCK_REALTIME
// #define MODE CLOCK_MONOTONIC
// #define MODE CLOCK_PROCESS_CPUTIME_ID
// #define MODE CLOCK_THREAD_CPUTIME_ID
int main(int argc, char ** argv)
{
int NumberOf = 1000;
int Mode = 0;
int Verbose = 0;
int c;
// l loops, m mode, h help, v verbose, k masK
int rc;
cpu_set_t mask;
int doMaskOperation = 0;
while ((c = getopt (argc, argv, "l:m:hkv")) != -1)
{
switch (c)
{
case 'l': // ell not one
NumberOf = atoi(optarg);
break;
case 'm':
Mode = atoi(optarg);
break;
case 'h':
cout << "Usage: <command> -l <int> -m <mode>" << endl
<< "where -l represents the number of loops and "
<< "-m represents the mode 0..3 inclusive" << endl
<< "0 is CLOCK_REALTIME" << endl
<< "1 CLOCK_MONOTONIC" << endl
<< "2 CLOCK_PROCESS_CPUTIME_ID" << endl
<< "3 CLOCK_THREAD_CPUTIME_ID" << endl;
break;
case 'v':
Verbose = 1;
break;
case 'k': // masK - sorry! Already using 'm'...
doMaskOperation = 1;
break;
case '?':
cerr << "XXX unimplemented! Sorry..." << endl;
break;
default:
abort();
}
}
if (doMaskOperation)
{
if (Verbose)
{
cout << "Setting CPU mask to CPU 0 only!" << endl;
}
CPU_ZERO(&mask);
CPU_SET(0,&mask);
assert((rc = sched_setaffinity(0,sizeof(mask),&mask))==0);
}
if (Verbose) {
cout << "Verbose: Mode in use: " << Mode << endl;
}
if (Verbose)
{
rc = sched_getaffinity(0,sizeof(mask),&mask);
// cout << "getaffinity rc is " << rc << endl;
// cout << "getaffinity mask is " << mask << endl;
int numOfCPUs = CPU_COUNT(&mask);
cout << "Number of CPU's is " << numOfCPUs << endl;
for (int i=0;i<sizeof(mask);++i) // sizeof(mask) is 128 RW 21 Nov 2014
{
if (CPU_ISSET(i,&mask))
{
cout << "CPU " << i << " is set" << endl;
}
//cout << "CPU " << i
// << " is " << (CPU_ISSET(i,&mask) ? "set " : "not set ") << endl;
}
}
clockid_t cpuClockID;
int err = clock_getcpuclockid(0,&cpuClockID);
if (Verbose)
{
cout << "Verbose: clock_getcpuclockid(0) returned err " << err << endl;
cout << "Verbose: clock_getcpuclockid(0) returned cpuClockID "
<< cpuClockID << endl;
}
timespec timeNumber[NumberOf];
for (int i=0;i<NumberOf;++i)
{
err = clock_gettime(Mode, &timeNumber[i]);
if (err != 0) {
int errSave = errno;
cerr << "errno is " << errSave
<< " NumberOf is " << NumberOf << endl;
cerr << strerror(errSave) << endl;
cerr << "Aborting due to this error" << endl;
abort();
}
}
for (int i=0;i<NumberOf-1;++i)
{
cout << timeNumber[i+1].tv_sec - timeNumber[i].tv_sec
+ (timeNumber[i+1].tv_nsec - timeNumber[i].tv_nsec) / 1000000000.
<< endl;
}
return 0;
}
合并时间2.awk
BEGIN {
PROCINFO["sorted_in"] = "@ind_num_asc"
}
{array[$0]++}
END {
lastX = -1;
first = 1;
for (x in array)
{
if (first) {
first = 0
lastX = x; lastCount = array[x];
} else {
delta = x - lastX;
if (delta < 2e-9) { # this is nasty floating point stuff!!
lastCount += array[x];
lastX = x
} else {
Cumulative += lastCount;
print lastX "\t" lastCount "\t" Cumulative
lastX = x;
lastCount = array[x];
}
}
}
print lastX "\t" lastCount "\t" Cumulative+lastCount
}
百分比.awk
{ # input is $1 a time interval $2 an observed frequency (i.e. count)
# $3 is a cumulative frequency
b[$1]=$2;
c[$1]=$3;
sum=sum+$2
}
END {
for (i in b) print i,b[i],c[i],(b[i]/sum)*100, (c[i]*100/sum);
}
差异.awk
NR==1 {
old=$1;next
}
{
print $1-old;
old=$1
}