【发布时间】:2019-11-11 09:28:23
【问题描述】:
当我测试具有明显、较慢、蛮力替代方案的函数时,我经常发现编写这两个函数并在调试标志打开时验证输出是否匹配很有帮助。在 C 中,它可能看起来像这样:
#include <inttypes.h>
#include <stdio.h>
#ifdef NDEBUG
#define _rsqrt rsqrt
#else
#include <assert.h>
#include <math.h>
#endif
// https://en.wikipedia.org/wiki/Fast_inverse_square_root
float _rsqrt(float number) {
const float x2 = number * 0.5F;
const float threehalfs = 1.5F;
union {
float f;
uint32_t i;
} conv = {number}; // member 'f' set to value of 'number'.
// approximation via Newton's method
conv.i = 0x5f3759df - (conv.i >> 1);
conv.f *= (threehalfs - (x2 * conv.f * conv.f));
return conv.f;
}
#ifndef NDEBUG
float rsqrt(float number) {
float res = _rsqrt(number);
// brute force solution to verify
float correct = 1 / sqrt(number);
// make sure the approximation is within 1% of correct
float err = fabs(res - correct) / correct;
assert(err < 0.01);
// for exposition sake: large scale systems would verify quietly
printf("DEBUG: rsqrt(%f) -> %f error\n", number, err);
return res;
}
#endif
float graphics_code() {
// graphics code that invokes rsqrt a bunch of different ways
float total = 0;
for (float i = 1; i < 10; i++)
total += rsqrt(i);
return total;
}
int main(int argc, char *argv[]) {
printf("%f\n", graphics_code());
return 0;
}
并且执行可能看起来像这样(如果上面的代码在 tmp.c 中):
$ clang tmp.c -o tmp -lm && ./tmp # debug mode
DEBUG: rsqrt(1.000000) -> 0.001693 error
DEBUG: rsqrt(2.000000) -> 0.000250 error
DEBUG: rsqrt(3.000000) -> 0.000872 error
DEBUG: rsqrt(4.000000) -> 0.001693 error
DEBUG: rsqrt(5.000000) -> 0.000162 error
DEBUG: rsqrt(6.000000) -> 0.001389 error
DEBUG: rsqrt(7.000000) -> 0.001377 error
DEBUG: rsqrt(8.000000) -> 0.000250 error
DEBUG: rsqrt(9.000000) -> 0.001140 error
4.699923
$ clang tmp.c -o tmp -lm -O3 -DNDEBUG && ./tmp # production mode
4.699923
除了单元测试和集成测试之外,我还喜欢这样做,因为它使很多错误的来源更加明显。它将捕获我可能忘记进行单元测试的边界情况,并且自然会扩展到我将来可能需要的任何更复杂情况的范围(例如,如果灯光设置发生变化并且我需要更高值的准确性)。
我正在学习 Rust,我真的很喜欢在测试和生产代码之间建立的原生利益分离。我正在尝试做与上述类似的事情,但无法弄清楚最好的方法是什么。根据我在this thread 中收集的信息,我可能可以在源代码中使用macro_rules! 和#[cfg!( ... )] 的某种组合来实现,但感觉就像我会打破测试/生产障碍。理想情况下,我希望能够在已经定义的函数周围放置一个验证包装器,但仅用于测试。宏和cfg 是我最好的选择吗?我可以在测试时重新定义导入包的默认命名空间,还是用宏做一些更聪明的事情?我知道通常文件不应该能够修改导入的链接方式,但是测试有例外吗?如果正在测试导入它的模块,我还希望它被包装怎么办?
我也愿意接受这样的回应,即这是一种不好的测试/验证方式,但请说明我上面提到的优点。 (或者作为奖励,有没有办法改进 C 代码?)
如果目前无法做到这一点,那么进入功能请求是否合理?
【问题讨论】: