背景

最近在改写一个加密算法,算法不可避免用到了随机数,但是Lazarus默认的随机种子初始化函数Randomize是依赖于GetTickCount的,而GetTickCount的时间精度是毫秒级的(windows平台实际大约是16ms级),日常是够用的,但在加密算法中就显得太过粗糙了。

关于GetTickCount的时间精度,详见Delphi中的延时在Lazarus中分析Windows和Linux的延时

在以上文章中有提到,windows平台下可以使用QueryPerformanceFrequencyQueryPerformanceCounter获取高精度的时间,事实上是微秒级的时间(本机实测是0.1微秒级,即百纳秒)。

考虑跨平台的话,如何获取非windows平台的高精度时间就变得很有必要了。

解决方案

搜索资料及查看Lazarus源码,找到了fpgettimeofdayclock_gettime两个函数。

fpgettimeofday

fpgettimeofday实际上就是UNIX平台的gettimeofday,只引用unix单元即可。其定义如下:

1
function fpgettimeofday(tp: ptimeval;tzp:ptimezone):cint; external name 'FPC_SYSC_GETTIMEOFDAY';

其参数ptimeval的定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  timeval = record
    tv_sec:time_t;
{$ifdef CPUSPARC64}
    tv_usec:cint;
{$else CPUSPARC64}
    tv_usec:clong;
{$endif CPUSPARC64}
  end;
  ptimeval = ^timeval;
  TTimeVal = timeval;

另外,unix平台的GetTickCount64用到了fpgettimeofday

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function GetTickCount64: QWord;
var
  tp: TTimeVal;
  {$IFDEF HAVECLOCKGETTIME}
  ts: TTimeSpec;
  {$ENDIF}
  
begin
 {$IFDEF HAVECLOCKGETTIME}
   if clock_gettime(CLOCK_MONOTONIC, @ts)=0 then
     begin
     Result := (Int64(ts.tv_sec) * 1000) + (ts.tv_nsec div 1000000);
     exit;
     end;
 {$ENDIF}
  fpgettimeofday(@tp, nil);
  Result := (Int64(tp.tv_sec) * 1000) + (tp.tv_usec div 1000);
end;

从其命名及使用中可确定,使用fpgettimeofday获取到的时间精度为微秒级。

clock_gettime

GetTickCount64也用到了clock_gettime,其参数ptimespec的定义如下:

1
2
3
4
5
6
  timespec = record
    tv_sec   : time_t;
    tv_nsec  : clong;
  end;
  ptimespec = ^timespec;
  TTimeSpec = timespec;

从其命名及使用从其命名及使用中可确定,使用clock_gettime获取到的时间精度为纳秒级。

注意:clock_gettime并非是在unix单元中定义的,反而是在linuxfreebsd中分别定义的,因此,使用入要配合合适的编译指令共同使用。

关于Randomize

在查看源码过程中发现,linux平台的Randomize并非是依赖于GetTickCount,实现如下:

1
2
3
4
Procedure Randomize;
Begin
  randseed:=longint(Fptime(nil));
End; 

Fptime的实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Fptime(tloc:pTime_t): Time_t; [public, alias : 'FPC_SYSC_TIME'];
{$if defined(generic_linux_syscalls) or defined(FPC_USEGETTIMEOFDAY)}
VAR tv     : timeval;
    tz     : timezone;
    retval : longint;

begin
  Retval:=do_syscall(syscall_nr_gettimeofday,TSysParam(@tv),TSysParam(@tz));
  If retval=-1 then
   Fptime:=-1
  else
   Begin
   If Assigned(tloc) Then
     TLoc^:=tv.tv_sec;
    Fptime:=tv.tv_sec;
   End;
End;
{$else FPC_USEGETTIMEOFDAY}
begin
  Fptime:=do_syscall(syscall_nr_time,TSysParam(tloc));
end;
{$endif FPC_USEGETTIMEOFDAY}

从中可以看出,其时间精度也是微秒级的。

疑问:Fptime虽然是System单元通过多层{$I}指令引入的,但却无法直接调用,具体原因尚未进行深入探究。