带有 boost::multiprecision 的单元测试

Posted

技术标签:

【中文标题】带有 boost::multiprecision 的单元测试【英文标题】:Unit tests with boost::multiprecision 【发布时间】:2020-07-04 06:33:23 【问题描述】:

自从调整了一些代码以启用多精度后,我的一些单元测试开始失败。头文件:

#ifndef SCRATCH_UNITTESTBOOST_INCLUDED
#define SCRATCH_UNITTESTBOOST_INCLUDED

#include <boost/multiprecision/cpp_dec_float.hpp>
// typedef double FLOAT;
typedef boost::multiprecision::cpp_dec_float_50 FLOAT;
const FLOAT ONE(FLOAT(1));

struct Rect

    Rect(const FLOAT &width, const FLOAT &height) : Width(width), Height(height);
    FLOAT getArea() const  return Width * Height; 
    FLOAT Width, Height;
;
#endif

主测试文件:

#define BOOST_TEST_DYN_LINK
#define BOOST_TEST_MODULE RectTest
#include <boost/test/unit_test.hpp>
#include "SCRATCH_UnitTestBoost.h"
namespace utf = boost::unit_test;

// Failing
BOOST_AUTO_TEST_CASE(AreaTest1)

    Rect R(ONE / 2, ONE / 3);
    FLOAT expected_area = (ONE / 2) * (ONE / 3);

    std::cout << std::setprecision(std::numeric_limits<FLOAT>::digits10) << std::showpoint;
    std::cout << "Expected: " << expected_area << std::endl;
    std::cout << "Actual  : " << R.getArea() << std::endl;

    // BOOST_CHECK_EQUAL(expected_area, R.getArea());
    BOOST_TEST(expected_area == R.getArea());


// Tolerance has no effect?
BOOST_AUTO_TEST_CASE(AreaTestTol, *utf::tolerance(1e-40))

    Rect R(ONE / 2, ONE / 3);
    FLOAT expected_area = (ONE / 2) * (ONE / 3);
    BOOST_TEST(expected_area == R.getArea());


// Passing
BOOST_AUTO_TEST_CASE(AreaTest2)

    Rect R(ONE / 7, ONE / 2);
    FLOAT expected_area = (ONE / 7) * (ONE / 2);
    BOOST_CHECK_EQUAL(expected_area, R.getArea());

请注意,当将FLOAT 定义为double 类型时,所有测试都会通过。让我感到困惑的是,当打印准确的预期值和实际值(参见 AreaTest1)时,我们会看到相同的结果。但是BOOST_TEST报的错误是:

    error: in "AreaTest1": check expected_area == R.getArea() has failed 
        [0.16666666666666666666666666666666666666666666666666666666666666666666666666666666 != 
         0.16666666666666666666666666666666666666666666666666666666666666666666666672236366]

g++ SCRATCH_UnitTestBoost.cpp -o utb.o -lboost_unit_test_framework编译。

问题:

    为什么测试失败了? 为什么在AreaTestTol 中使用tolerance 没有提供here 记录的输出?

相关信息:

    Tolerances with floating point comparison Gotchas with multiprecision types

【问题讨论】:

【参考方案1】:

两个问题:

差异从何而来 如何应用 epsilon?

差异从何而来

Boost Multiprecision 使用模板表达式来推迟评估。

另外,您选择了一些不能以 10 为底数精确表示的有理分数(cpp_dec_float 使用十进制,因此以 10 为底数)。

这意味着当你这样做时

T x = 1/3;
T y = 1/7;

这实际上会不精确地近似这两个分数。

这样做:

T z = 1/3 * 1/7;

实际上会计算右侧的表达式模板,所以不是像之前计算 xy 这样的临时变量,右侧有一个类型:

expression&lt;detail::multiplies, detail::expression&lt;?&gt;, detail::expression&lt;?&gt;, [2 * ...]&gt;

这是 actual 类型的缩写:

boost::multiprecision::detail::expression<
    boost::multiprecision::detail::multiplies,
    boost::multiprecision::detail::expression<
        boost::multiprecision::detail::divide_immediates,
        boost::multiprecision::number<boost::multiprecision::backends::cpp_dec_float<50u,
            int, void>, (boost::multiprecision::expression_template_option)1>, int,
            void, void>,
    boost::multiprecision::detail::expression<
        boost::multiprecision::detail::divide_immediates,
        boost::multiprecision::number<boost::multiprecision::backends::cpp_dec_float<50u,
            int, void>, (boost::multiprecision::expression_template_option)1>, int,
            void, void>,
    void, void>

长话短说,这就是您想要的,因为它可以节省您的工作量并保持更好的准确性,因为表达式首先被标准化为1/(3*7) 所以1/21

这就是你与众不同的地方。通过以下任一方式修复它:

    关闭表达式模板

    using T = boost::multiprecision::number<
        boost::multiprecision::cpp_dec_float<50>,
        boost::multiprecision::et_off > >;
    

    重写表达式以等效于您的实现:

    T expected_area = T(ONE / 7) * T(ONE / 2);
    T expected_area = (ONE / 7).eval() * (ONE / 2).eval();
    

应用公差

我发现很难解析 Boost Unit Test 文档,但这里是经验数据:

BOOST_CHECK_EQUAL(expected_area, R.getArea());
T const eps = std::numeric_limits<T>::epsilon();
BOOST_CHECK_CLOSE(expected_area, R.getArea(), eps);
BOOST_TEST(expected_area == R.getArea(), tt::tolerance(eps));

第一个失败,最后两​​个通过。确实,除此之外,下面两个也失败了:

BOOST_CHECK_EQUAL(expected_area, R.getArea());
BOOST_TEST(expected_area == R.getArea());

所以看起来在utf::tolerance 装饰器生效之前必须做一些事情。使用本机双精度进行测试告诉我,只有 BOOST_TEST 隐式应用了容差。于是潜入了预处理扩展:

    ::boost::unit_test::unit_test_log.set_checkpoint(
        ::boost::unit_test::const_string(
            "/home/sehe/Projects/***/test.cpp",
            sizeof("/home/sehe/Projects/***/test.cpp") - 1),
        static_cast<std::size_t>(42));
    ::boost::test_tools::tt_detail::report_assertion(
        (::boost::test_tools::assertion::seed()->*a == b).evaluate(),
        (::boost::unit_test::lazy_ostream::instance()
         << ::boost::unit_test::const_string("a == b", sizeof("a == b") - 1)),
        ::boost::unit_test::const_string(
            "/home/sehe/Projects/***/test.cpp",
            sizeof("/home/sehe/Projects/***/test.cpp") - 1),
        static_cast<std::size_t>(42), ::boost::test_tools::tt_detail::CHECK,
        ::boost::test_tools::tt_detail::CHECK_BUILT_ASSERTION, 0);
 while (::boost::test_tools::tt_detail::dummy_cond());

深入挖掘,我遇到了:

/*!@brief Indicates if a type can be compared using a tolerance scheme
 *
 * This is a metafunction that should evaluate to @c mpl::true_ if the type
 * @c T can be compared using a tolerance based method, typically for floating point
 * types.
 *
 * This metafunction can be specialized further to declare user types that are
 * floating point (eg. boost.multiprecision).
 */
template <typename T>
struct tolerance_based : tolerance_based_delegate<T, !is_array<T>::value && !is_abstract_class_or_function<T>::value>::type ;

我们有它!但是没有,

static_assert(boost::math::fpc::tolerance_based<double>::value);
static_assert(boost::math::fpc::tolerance_based<cpp_dec_float_50>::value);

两者都已经通过了。嗯。

查看装饰器,我注意到注入到夹具上下文中的容差是typed

通过实验我得出的结论是容差装饰器需要与比较中的操作数具有相同的静态类型参数才能生效。

这实际上可能非常有用(您可以对不同的浮点类型有不同的隐式容差),但也非常令人惊讶。

TL;DR

这是固定的完整测试集,供您欣赏:

考虑评估顺序和对准确性的影响 使用utf::tolerance(v) 中的静态类型来匹配您的操作数 不要使用 BOOST_CHECK_EQUAL 进行基于容差的比较 我建议使用明确的test_tools::tolerance 而不是依赖“环境”容差。毕竟,我们想要测试我们的代码,而不是测试框架

活在 Coliru 上

template <typename T> struct Rect 
    Rect(const T &width, const T &height) : width(width), height(height);
    T getArea() const  return width * height; 
  private:
    T width, height;
;

#define BOOST_TEST_DYN_LINK
#define BOOST_TEST_MODULE RectTest
#include <boost/multiprecision/cpp_dec_float.hpp>
using DecFloat = boost::multiprecision::cpp_dec_float_50;

#include <boost/test/unit_test.hpp>
namespace utf = boost::unit_test;
namespace tt = boost::test_tools;

namespace 
    template <typename T>
    static inline const T Eps = std::numeric_limits<T>::epsilon();

    template <typename T> struct Fixture 
        T const epsilon = Eps<T>;
        T const ONE     = 1;
        using Rect      = ::Rect<T>;

        void checkArea(int wdenom, int hdenom) const 
            auto w = ONE/wdenom; // could be expression templates
            auto h = ONE/hdenom;

            Rect const R(w, h);
            T expect = w*h;
            BOOST_TEST(expect == R.getArea(), "1/" << wdenom << " x " << "1/" << hdenom);

            // I'd prefer explicit toleranc
            BOOST_TEST(expect == R.getArea(), tt::tolerance(epsilon));
        
    ;



BOOST_AUTO_TEST_SUITE(Rectangles)
    BOOST_FIXTURE_TEST_SUITE(Double, Fixture<double>, *utf::tolerance(Eps<double>))
        BOOST_AUTO_TEST_CASE(check2_3)    checkArea(2, 3); 
        BOOST_AUTO_TEST_CASE(check7_2)    checkArea(7, 2); 
        BOOST_AUTO_TEST_CASE(check57_31)  checkArea(57, 31); 
    BOOST_AUTO_TEST_SUITE_END()
    BOOST_FIXTURE_TEST_SUITE(MultiPrecision, Fixture<DecFloat>, *utf::tolerance(Eps<DecFloat>))
        BOOST_AUTO_TEST_CASE(check2_3)    checkArea(2, 3); 
        BOOST_AUTO_TEST_CASE(check7_2)    checkArea(7, 2); 
        BOOST_AUTO_TEST_CASE(check57_31)  checkArea(57, 31); 
    BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_SUITE_END()

打印

【讨论】:

多么好的答案!不确定我是否理解第一部分中表达式模板用例的目的。但是考虑到术语,我能够找到this 页面。 另一件事.. 对于在多精度和常规类型之间切换的应用程序,所有类都应该是模板类吗?仅拥有全局指定精度类型的typedefs 不一样吗?毕竟,混合为不同类型定义的不同对象听起来很冒险。 是一样的,但是这样我就不需要重新编译来运行不同的测试了。为了测试覆盖率,我认为这是一件好事(因为可能会忘记需要额外步骤的测试)。 另外,拥有模板并不意味着您需要混合任何东西。就像拥有std::vector&lt;std::string&gt;std::vector&lt;int&gt; 一样不会导致问题。两者都可以单独使用。我想你的代码库会有using Rect = Impl::Rect&lt;FLOAT&gt;;(除了我会为宏保留全大写) 你明白了。这真的取决于你在做什么。在某些情况下,使用表达式模板对于性能或正确性至关重要。我认为它只会让这种特定类型的单元测试更容易准确地表达,因为它会牺牲一些精度。

以上是关于带有 boost::multiprecision 的单元测试的主要内容,如果未能解决你的问题,请参考以下文章

boost::multiprecision 中随机,种子无法编译

具有固定精度的重载 boost::multiprecision::pow

具有两个 cpp_int 值的 boost::multiprecision::pow

C++ boost multiprecision类型转换 u256转double

为啥在评估复值积分时 boost::multiprecision::exp 会卡住?

如何将字节数组转换为 boost::multiprecision::uint128_t?