处理纳秒计数器时,很容易写出下面这一行:
seconds = (double(t)-double(t(1)))/1e9;
如果 t 是很大的 uint64 时间戳,转换成 double 的那一刻,低位可能已经丢失。后面的减法无法恢复它们。本文实现一个合同明确的小函数:先验证顺序,用 uint64 做相对减法,检查跨度,最后转换成秒。
全部实验在 Windows 原生 MATLAB R2025b 中执行,验证数值表示与输入边界,不涉及实际采集设备或硬件时钟性能。
1. 问题出在转换顺序
double 可连续精确表示的整数区间包含 [-2^53,2^53]。uint64 的范围更大。以 2^60 为基准,向上的相邻 double 间距是 256,因而 +1、+2、+3 这类差值在预先转换时会消失。
构造样例也要遵守相同原则:
base = bitshift(uint64(1),60);
t = base + uint64([0 1 2 3 1000]);
不要写 uint64(2^60+1) 来声称构造了精确的 2^60+1。括号内先按 double 运算,丢失低位之后再转 uint64 已经来不及。大整数必须从原始整数数据或不会丢位的整数运算得到。
2. 函数合同
relative_seconds_u64(t,ticksPerSecond) 接收非空 uint64 向量,输出保留行列形状,以第一个时间戳为零点。
约束如下:
- t 必须按顺序不减,允许重复时间戳。
- 不接受倒序、计数器回绕或复位。函数不会猜测它们的原因。
- 最大相对跨度不能超过 2^53 tick,等于该边界时允许。
- ticksPerSecond 必须为正有限 double 标量。
- 若转换后的秒数溢出到非有限值,也会报错;仅仅“正有限”的极小频率并不足以保证可转换。
这些约束换来一个清楚的保证:被转换的每个整数 tick 差都可被 double 精确表示。除法转成秒仍然服从浮点舍入,不保证每个十进制小数精确,也不保证大跨度下的相邻 tick 在秒域仍可区分。
3. 完整实现
保存为 relative_seconds_u64.m:
function seconds=relative_seconds_u64(t,ticksPerSecond)
% Nondecreasing, no-wrap timestamps. Preserve shape and exact tick deltas.
validateattributes(t,{'uint64'},{'vector','nonempty'});
validateattributes(ticksPerSecond,{'double'},{'real','scalar','finite','positive'});
if any(t(2:end)<t(1:end-1))
error('ticks:order','Timestamps must be nondecreasing; unwrap separately.');
end
delta=t-t(1);
if any(delta>bitshift(uint64(1),53))
error('ticks:span','Span must not exceed 2^53 ticks.');
end
seconds=double(delta)/ticksPerSecond;
if any(~isfinite(seconds))
error('ticks:overflow','Seconds overflow; choose a valid scale or smaller span.');
end
end
顺序检查必须在减法之前。MATLAB 的无符号整数减法不会提供负数差值;若先对倒序数据相减,负结果可能被饱和为零。先检查相邻大小关系,才能避免把倒序悄悄伪装成重复时间戳。
在不减序列中,所有 t 均不小于首点,因此整数减法不存在负结果;两个 uint64 数的非负差也不会超过 uint64 上限。此后再执行 2^53 跨度检查。
4. 可复现测试与独立预期
测试中的大基准和小偏移分开生成。随机测试的预期来自生成时已知的 offsets,而不是再调用被测函数的减法过程。固定随机种子,1000 组、每组 100 点,覆盖多种大基准上的小跨度计数。
保存为 test_timestamps.m:
function test_timestamps(f)
if nargin==0, f=@relative_seconds_u64; end
base=bitshift(uint64(1),60); t=base+uint64([0 1 2 3 1000]);
s=f(t,1e9); want=[0 1 2 3 1000]/1e9;
assert(isequal(s,want),'test:precision','Lost low timestamp bits');
assert(isequal(f(uint64([4;4;9]),2),[0;0;2.5]));
assert(f(uint64(9),1)==0);
lim=bitshift(uint64(1),53); assert(isequal(f([uint64(0) lim],1),[0 2^53]));
mx=intmax('uint64'); assert(isequal(f([mx-uint64(2),mx],1),[0 2]));
invalid={uint64([]),[1 2],uint64([2 1]),[mx uint64(0)],uint64([1 2;3 4]),[uint64(0) lim+uint64(1)]};
for j=1:numel(invalid), mustError(@()f(invalid{j},1)); end
for rate={0,-1,Inf,NaN,[],single(1)}, mustError(@()f(uint64(1),rate{1})); end
mustError(@()f(uint64([0 1]),realmin*eps));
rng(20261110,'twister'); worst=0;
% Offsets are independently known small exact integers, not DUT subtraction.
for j=1:1000
offsets=sort(randi([0 1000000],1,100)); offsets(1)=0;
b=bitshift(uint64(randi([1 1000000])),40);
actual=f(b+uint64(offsets),1e6); expected=offsets/1e6;
assert(isequal(actual,expected)); worst=max(worst,max(abs(actual-expected)));
end
fprintf('TIMESTAMPS PASS: 5 deterministic, 13 invalid, 1000 x 100 random; max error=%.17g s\n',worst);
fprintf('LARGE BASE: correct='); fprintf(' %.17g',s); fprintf('\n');
end
function mustError(fn)
caught=false; try, fn(); catch, caught=true; end; assert(caught,'test:reject','Expected rejection');
end
保存下面故意错误的负控为 bad_timestamps.m。它用于证明精度断言确实能抓到“先转 double”的错误,不是生产实现:
function s=bad_timestamps(t,rate)
s=(double(t)-double(t(1)))/rate;
end
在上述文件目录运行 PowerShell,修改路径以适配本机安装:
& 'D:\tools\matlab\R2025b\bin\matlab.exe' -batch "test_timestamps" -logfile ticks_ok.log
$LASTEXITCODE
& 'D:\tools\matlab\R2025b\bin\matlab.exe' -batch "test_timestamps(@bad_timestamps)" -logfile ticks_bad.log
$LASTEXITCODE
5. 实测结果
MATLAB 版本为 25.2.0.2998904 (R2025b)。5 个确定性场景、13 个非法输入场景、1000×100 个随机点全部通过。随机数据结果与独立生成偏移除以 1e6 的预期逐元素完全一致,比较得到的最大误差为 0 秒。这里的“0”是相对于同样使用 double 除法的预期,不是声称浮点秒数等于精确有理数。
对于基准 2^60、偏移 [0 1 2 3 1000] tick、频率 1e9 tick/s,正确算法的十进制概念结果是 [0 1e-9 2e-9 3e-9 1e-6] 秒。负控则得到 [0 0 0 0 1.024e-6] 秒,前三个非零微小偏移被吞掉,1000 tick 被量化成 1024 tick。
测试确认了以下边界:重复值允许;单点输出零;跨度恰好 2^53 tick 允许;跨度 2^53+1 tick 拒绝;接近 intmax(‘uint64’) 但跨度仅 2 tick 的序列仍可处理;从最大 uint64 回到零明确拒绝。极小频率 realmin*eps 使一个 tick 对应的秒数溢出,也明确拒绝。
联合正式测试进程退出码为 0。负控触发运行期 test:precision 断言,错误文本为 Lost low timestamp bits,退出码为 1,失败发生在数值结果检查,不是语法或编译阶段。
6. 哪些问题需要另一层处理
如果设备计数器会回绕,需要知道计数器位宽、最大采样间隔,以及两次样本间能否发生多次回绕,才能建立展开策略。只有两次读数,通常无法区分真正倒序、计数器复位和回绕。本文选择拒绝,不使用排序来掩盖时间顺序问题。
2^53 tick 是为了保证所有整数差可精确转换的保守边界,超过它也有部分整数可表示,但不能对任意 tick 差做同样保证。在 1 GHz 计数率下,2^53 tick 约为 9007199.254740992 秒,约 104.25 天。更长记录可分段使用局部原点,或保留整数差与时间单位,把转换推迟到显示和分析阶段。
即使跨度满足限制,秒域除法仍可能舍入,极端量级可能进入次正规数范围,仍有舍入。对于本合同内非零整数差和有限正频率,非零秒数不会下溢为零。需要单 tick 的严格比较、精确累计或跨设备同步时,应保留 uint64 计数和时钟元数据。该函数没有校正时钟漂移、时区、闰秒或多个时钟源之间的偏差。
7. 官方参考
- MathWorks flintmax 文档:https://www.mathworks.com/help/matlab/ref/flintmax.html ,说明 double 的最大连续精确整数边界为 2^53。
- MathWorks 整数数据类型与运算:https://www.mathworks.com/help/matlab/matlab_prog/integers.html ,说明整数运算饱和、数值字面量默认 double,以及大整数转换前的精度问题。
191



被折叠的 条评论
为什么被折叠?



