使用 boost::multiprecision 进行单元测试

Unit tests with boost::multiprecision

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

#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());
}

注意,当定义FLOATdouble类型时,所有的测试都通过了。令我困惑的是,当打印准确的预期值和实际值时(请参阅 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 编译。

问题:

  1. 为什么测试失败?
  2. 为什么在 AreaTestTol 中使用 tolerance 没有给出记录的输出 here

相关信息:

  1. Tolerances with floating point comparison
  2. Gotchas with multiprecision types

两期:

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

差异从何而来

Boost Multiprecision 使用模板表达式来延迟计算。

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

这意味着当你这样做时

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

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

这样做:

T z = 1/3 * 1/7;

实际上会计算右侧的 表达式模板 ,因此不像之前计算 x ans y 那样的临时变量,右侧有一种类型:

expression<detail::multiplies, detail::expression<?>, detail::expression<?>, [2 * ...]>

这是实际类型的缩写:

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.

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

  1. 关闭表达式模板

    using T = boost::multiprecision::number<
        boost::multiprecision::cpp_dec_float<50>,
        boost::multiprecision::et_off > >;
    
  2. 重写表达式以等效于您的实现:

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

应用 Tole运行ce

我发现很难解析关于此的 Boost 单元测试文档,但这里有经验数据:

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 隐式应用 tole运行ce。所以深入研究预处理扩展:

    ::boost::unit_test::unit_test_log.set_checkpoint(
        ::boost::unit_test::const_string(
            "/home/sehe/Projects/Whosebug/test.cpp",
            sizeof("/home/sehe/Projects/Whosebug/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/Whosebug/test.cpp",
            sizeof("/home/sehe/Projects/Whosebug/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);

两个都已经通过了。嗯。

查看装饰器,我注意到注入到夹具上下文中的 tole运行ce 是 typed.

根据实验,我得出的结论是 tole运行ce 装饰器 需要 具有相同的 static 类型参数 比较中的ope运行ds才能生效。

这实际上可能非常有用(对于不同的浮点类型你可以有不同的隐式 tole运行ces),但它也很令人惊讶。

TL;DR

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

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

靠 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()

版画