【问题标题】:clock_t() overflow on 32-bit machineclock_t() 在 32 位机器上溢出
【发布时间】:2022-11-17 07:39:38
【问题描述】:

出于统计目的,我想以微秒为单位累积用于程序功能的整个 CPU 时间。它必须在两个系统中工作,一个是sizeof(clock_t) = 8 (RedHat),另一个是sizeof(clock_t) = 4 (AIX)。在两台机器中,clock_t 是有符号整数类型,CLOCKS_PER_SEC = 1000000(= 1 微秒,但我没有在代码中做这样的假设,而是使用宏)。

我所拥有的相当于这样的东西(但封装在一些花哨的类中):

typedef unsigned long long u64;
u64 accum_ticks = 0;

void f()
{
   clock_t beg = clock();
   work();
   clock_t end = clock();

   accum_ticks += (u64)(end - beg); // (1)
}

u64 elapsed_CPU_us()
{
   return accum_tick * 1e+6 / CLOCKS_PER_SEC;
}

但是,在 clock_tint 的 32 位 AIX 机器中,它将在 35 分钟 47 秒后溢出。假设在某些调用中,beg 等于程序启动后的 35 分钟 43 秒,而 work() 占用 10 CPU 秒,导致 end 溢出。从现在起,我可以信任(1) 电话以及随后拨打f() 电话吗?当然,f() 保证不会超过 35 分钟的执行时间。

如果即使在我的特定机器上我也根本不信任(1)行,我有什么替代方案不意味着导入任何第三方库? (我无法将库复制粘贴到系统中,也无法使用 <chrono>,因为在我们的 AIX 机器中它不可用)。

笔记:我可以使用内核标头,我需要的精度以微秒为单位。

【问题讨论】:

  • 不要用 C 标记标记使用仅在 C++ 中有效的符号的问题。
  • unsigned long long 类型是 C99 以来 C 的一部分,但是,是的,代码现在是有效的 C(如果您在范围内有适当的 using namespace,则可能是有效的 C++)。 OTOH,除非您的问题是关于两种语言的互通,否则您仍然应该选择两种语言中的一种。
  • 时钟计数器不会溢出:它像无符号一样回绕,并继续计数。
  • 假设 beg = 0x7fffffffend = 0x80000003,你得到 80000003 - 0x7fffffff4。如果您使用未签名的elapsed多变的以确保差异是正确的。或者假设 beg = 0xffffffffend = 0x0000003,你得到 00000003 - 0xffffffff4
  • @Peregring-lk:有没有理由专门使用clock? POSIX 提供了getrusage,它有更好的规范(clock 没有指定是否包括等待子进程时间,没有指定clock_t 是整数还是浮点数更不用说大小等。 ). getrusage 允许您指定是否包括子进程使用的资源,分别分解用户 CPU 和系统 CPU 时间,并指定用户和系统 CPU 时间都将表示为一个结构,该结构结合了 time_t 秒计数具有整数微秒计数。

标签: c++ c time


【解决方案1】:

另一个建议:不要使用clock。它太不明确了,几乎不可能编写完全可移植的代码,处理 32 位整数clock_t、整数与浮点数clock_t 等的可能环绕(当你写它的时候,你已经写了如此丑陋,你已经失去了 clock 提供的任何简单性)。

相反,use getrusage。它并不完美,它可能比您严格需要的多一点,但是:

  1. 它返回的时间保证相对于0进行操作(其中clock在程序开头返回的值可以是任何值)
  2. 它允许您指定是否要包含您等待的子进程的统计信息(clock 以不可移植的方式包含或不包含)
  3. 它将用户和系统CPU时间分开;你可以使用其中之一,或两者都使用,你的选择
  4. 每次都明确表示为一对值,time_t秒数和suseconds_t额外微秒数。由于它不会尝试将总微秒计数编码为单个time_t/clock_t(可能是 32 位),因此在您达到至少 68 年的 CPU 时间之前不会发生回绕(如果您在具有 32 位time_t 的系统上管理它,我想了解您的 IT 人员;我能想象的唯一方法是在具有数百个内核、运行数周的系统上实现这一目标,而任何此类系统此时都是 64 位的观点)。
  5. 您需要的部分结果由 POSIX 指定,因此它可以移植到除 Windows 以外的任何地方(在为 Windows 编译时,您必须编写预处理器控制代码以切换到 GetProcessTimes

    方便的是,由于您使用的是 POSIX 系统(我认为?),clock 已经表示为微秒,而不是真正的刻度(POSIX 指定 CLOCKS_PER_SEC 等于 1000000),因此值已经对齐。您可以将函数重写为:

    #include <sys/time.h>
    #include <sys/resource.h>
    
    static inline u64 elapsed(const struct timeval *beg, const struct timeval *end)
    {
        return (end->tv_sec - beg->tv_sec) * 1000000ULL + (end->tv_usec - beg->tv_usec);
    }
    
    void f()
    {
       struct rusage beg, end;
       // Not checking return codes, because only two documented failure cases are passing
       // an unmapped memory address for the struct addr or an invalid who flag, neither of which
       // we're doing, easily verified by inspection
       getrusage(RUSAGE_SELF, &beg);
       work();
       getrusage(RUSAGE_SELF, &end);
    
       accum_ticks += elapsed(&beg.ru_utime, &end.ru_utime);
       // And if you want to include system time as well, add:
       accum_ticks += elapsed(&beg.ru_stime, &end.ru_stime);
    }
    
    u64 elapsed_CPU_us()
    {
       return accum_ticks; // It's already stored natively in microseconds
    }
    

    在 Linux 2.6.26+ 上,您可以将 RUSAGE_SELF 替换为 RUSAGE_THREAD 以限制仅由调用线程单独使用的资源,而不仅仅是调用进程(如果其他线程正在执行不相关的工作而您不这样做,这可能会有所帮助)不想他们的统计数据污染你的),以换取更少的便携性。

    是的,计算时间需要做更多的工作(两次加法/减法,一次乘以常数,如果您同时需要用户时间和系统时间,则加倍,其中clock最简单的用法是一次减法),但是:

    1. 处理clock wraparound 增加了更多工作(和分支工作,这段代码没有;诚然,这是一个相当可预测的分支),缩小差距
    2. 整数乘法与现代芯片上的加法和减法大致一样便宜(最新的 x86-64 芯片在单个时钟周期内执行整数乘法),因此您不会增加多个数量级的工作量,作为交换,您获得更多控制权、更多保证和更大的可移植性

      注意:您可能会看到使用时钟 ID CLOCK_PROCESS_CPUTIME_IDclock_gettime 的代码,当您只想要总 CPU 时间而不是按用户与系统划分时,这将简化您的代码,而没有 getrusage 提供的所有其他内容(也许它会更快,仅仅是因为检索的数据更少)。不幸的是,虽然 clock_gettime 由 POSIX 保证,但 CLOCK_PROCESS_CPUTIME_ID 时钟 ID 不是,因此您不能在所有 POSIX 系统上可靠地使用它(至少 FreeBSD 似乎缺少它)。我们依赖的 getrusage 的所有部分都是完全标准的,所以它是安全的。

【讨论】:

  • 次要:(end-&gt;tv_sec - beg-&gt;tv_sec) * 1000000ULL + end-&gt;tv_usec - beg-&gt;tv_usec 可以使用更窄,也许更快的数学,(end-&gt;tv_sec - beg-&gt;tv_sec) * 1000000ULL + (end-&gt;tv_usec - beg-&gt;tv_usec)
  • @chux-ReinstateMonica:我避免这样做只是因为我不想验证当计算结果为负时(至少经过一秒后,end 微秒可能更小),@987654354 的行为@ 将是 100% 可移植的。这可能是安全的,但纯粹以积极的价值观工作可以消除我的疑虑;实际上,至少在 x86-64 上,64 位加法/减法的性能与 32 位没有明显区别。您知道标准是否保证安全吗?我永远记不起这些细节。
  • 只要.tv_usec(有符号整数类型)在 [0...1000000000) 范围内,(end-&gt;tv_usec - beg-&gt;tv_usec) 就是安全的。
  • @chux-ReinstateMonica:是的,那部分绝对安全。问题是,如果计算结果为负(因为end-&gt;tv_usec小于beg-&gt;tv_usec),将较小的负值与较大的无符号值相加是否安全。我思考它是(对于匹配的大小,it is)但是它需要两者都变得无符号的额外复杂性从 32 位提升到 64 位使我成为小的怀疑。
  • 基本上,我不确定是否保证this program总是为所有 C 和 C++ 标准及其所有常见编译器打印 12345678901234567885(将 -5 添加到 12345678901234567890ULL 的结果)。
【解决方案2】:

unsigned long long(end - beg) 使用 clock_t 数学进行减法,这比 64 位数学更容易溢出。

建议在减法中使用long long数学。

//unsigned long long accum_ticks = 0;
//...
//accum_ticks += unsigned long long(end - beg);

long long accum_ticks = 0;
...
accum_ticks += 0LL + end - beg;

为了应对 clock_t 有时环绕,我们需要确定一个 CLOCK_MAX 适用于或者未签名clock_t。请注意,clock_t 可能是 FP,下面的方法是有问题的。

#define CLOCK_MAX _Generic(((clock_t) 0), 
  unsigned long: ULONG_MAX/2, 
  long: LONG_MAX, 
  unsigned: UINT_MAX/2, 
  int: INT_MAX, 
  unsigned short: USHRT_MAX/2, 
  short: SHRT_MAX 
  )


long long accum_ticks = 0;
...
long long diff = 0LL + end - beg;
if (diff < 0) {
  diff += 1LL + CLOCK_MAX + CLOCK_MAX;
}  
accum_ticks += diff;

如果调用之间的间隔小于或等于 1 个“换行”,则此方法有效。

【讨论】:

  • 我承认它更短,但是为了避免写(long long)end - beg(或 C++-ey,static_cast&lt;long long&gt;(end) - beg)而在等式中添加一个额外的无用操作数感觉很愚蠢。无论如何,OP 似乎担心的溢出是clock 本身溢出。
  • @ShadowRanger — 是的,OP 似乎担心 clock() 中的溢出,但这并不重要。就任何用户/程序员而言,clock() 中没有溢出;它可能只是一次返​​回一个大数字,下一次返回一个小数字。难点在于如何处理两个连续值大和小的计算。
  • @ShadowRanger,我发现0LL + end - beg(long long)end - beg更容易维护代码。考虑 clock_t 是否比 long long 宽。 0LL + end - beg; 仍然可以正确计算。 (long long)end - beg 没有。
  • @ShadowRanger 代码已修改。应对clock_t wrap-around is a portable fashion 有很多问题 - 一些在这里解决了。
  • @Peregring-lk 我不认为 u64 技巧会奏效 - 但也许会 - 我天气不好。注意:与其发明 u64,不如使用 stdint.h 中的 uint64_tunsigned long long 我比 64 宽。
猜你喜欢
  • 2014-05-08
  • 2011-02-11
  • 2017-09-01
  • 2010-10-12
  • 2018-04-18
  • 2011-08-08
  • 2011-04-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多