1

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

#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. 为什么使用toleranceinAreaTestTol没有给出此处记录的输出?

相关资料:

  1. 带浮点比较的公差
  2. 多精度类型的陷阱
4

1 回答 1

1

两个问题:

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

差异从何而来

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

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

这意味着当你做

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

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

这样做:

T z = 1/3 * 1/7;

实际上会计算右侧的表达式 template,所以不是像以前那样计算临时x变量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)so 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();
    

应用公差

我发现很难解析 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/stackoverflow/test.cpp",
            sizeof("/home/sehe/Projects/stackoverflow/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/stackoverflow/test.cpp",
            sizeof("/home/sehe/Projects/stackoverflow/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);

两者都已经通过了。唔。

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

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

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

TL;博士

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

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

住在科利鲁

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

印刷

在此处输入图像描述

于 2020-07-04T23:23:16.727 回答