Skip to content

PR#16 compiles extremely slowly in the Windows MSVC environment #17

Description

@FrozenLemonTee

问题描述

在 mcpplibs/primitives 的 PR #16(feat-explicit-conversion)中,Windows CI 明显慢于 Linux/macOS,且测试目标构建阶段出现超时/取消。

影响

观测到的现象

GitHub Actions(PR #16)

  • 运行 23537726928:
  • build-windows 约 20 分钟后 cancelled
  • build-linux ~1 分钟失败
  • build-macos ~2 分钟成功
  • 运行 23540994438:
  • build-windows 约 30 分钟后 cancelled
  • build-linux ~2 分钟成功
  • build-macos ~2 分钟成功
  • Windows 慢点集中在 primitives_test 构建阶段(Build and test step)

可能原因(待确认)

  1. PR Enhance type classification and simplify numeric conversion functions #16 的 Windows CI 脚本调整导致吞吐下降:
  • 并行度改为固定 -j2
  • 分阶段构建多个目标(core/test/example)
  1. 新增 conversion 模块与测试提升了模板/模块编译负载。

复现步骤

GitHub Actions 复现

  1. 打开 PR Enhance type classification and simplify numeric conversion functions #16。
  2. 触发 CI(或查看最近两次运行)。
  3. 观察 Windows job 在 Build and test 阶段长时间运行后 cancelled。

期望行为

  • Windows CI 的 primitives_test 能在超时前稳定完成。
  • conversion 测试开/关应能在同一 Windows CI 环境下进行可比的耗时评估。

建议的下一步

  1. 在 Windows job 暂时恢复更高并行度(如 $env:NUMBER_OF_PROCESSORS),并减少重复分段构建。
  2. 在 Windows CI 中做 A/B:
  • A: conversion tests enabled
  • B: conversion tests disabled
    比较 Windows primitives_test 纯构建耗时与总 job 耗时。

相关信息

Activity

  1. added 2 commits that reference this issue on Mar 26, 2026
    2452fba
    be07d3f
  2. FrozenLemonTee commented on Mar 26, 2026

    @FrozenLemonTee
    MemberAuthor

    排查报告

    1. 问题原因概括

    本次编译速度异常缓慢的根本原因,不是 conversion 模块里的算法本身运行复杂,而是 conversion 公共 API 的模板约束发生了重叠,导致编译器在模块编译阶段反复进行高成本的 concepts / subsumption / overload ordering 判定。

    具体来说:

    • 项目通过 underlying::traits<T> 将 builtin 数值类型纳入了 underlying_type 体系,因此 int、uint16_t、double 这类类型不仅满足 std_numeric,也满足 underlying_type / numeric_underlying_type。
    • 在这个前提下,conversion::checked_cast / saturating_cast / truncating_cast / exact_cast 同时提供了“builtin numeric 路径”和“underlying bridge 路径”两套公开重载。
    • 对于 builtin-to-builtin 的调用,例如 checked_cast<int>(42u)、saturating_cast<int>(NaN),两套候选同时成立,编译器需要做额外的约束归约与偏序比较。
    • 这一问题在单独的 basic_conversion_tests 中首先暴露;而当 primitive.impl 和 operations.operators 这两个高扇出热点模块重新接回 conversion 公共 API 后,实例化数量被迅速放大,最终表现为 Clang 和 Windows/MSVC 模块编译显著变慢甚至超时。

    2. 关键代码改动对比

    2.1 问题版本:公开 cast API 的候选集重叠

    旧版本中,公开 cast API 同时存在两套可见重载:

    template <numeric_underlying_type DestRep, numeric_underlying_type SrcRep>
    constexpr auto checked_cast(SrcRep value);
    
    template <underlying_type Dest, underlying_type Src>
    constexpr auto checked_cast(Src value);

    同时,builtin 类型又被注册进了 underlying::traits<T>:

    template <std_underlying_type T>
    struct underlying::traits<T> {
      using value_type = std::remove_cv_t<T>;
      using rep_type = value_type;
      static constexpr bool enabled = true;
    };

    这意味着 builtin 类型既是 std_numeric,也是 underlying_type。

    于是像下面这样的调用:

    conversion::checked_cast<int>(42u);
    conversion::saturating_cast<int>(std::numeric_limits<double>::quiet_NaN());

    会同时命中两套公开模板候选,编译器必须额外判断“哪一套更受约束”。

    2.2 修复版本:将 builtin 路径与 underlying 路径彻底拆开

    修复后的公开 API 改成了互斥的两层:

    template <std_numeric DestRep, std_numeric SrcRep>
    constexpr auto checked_cast(SrcRep value);
    
    template <underlying_type Dest, underlying_type Src>
      requires (!details::builtin_numeric_pair<std::remove_cv_t<Dest>,
                                               std::remove_cv_t<Src>>)
    constexpr auto checked_cast(Src value);

    同样的策略也应用到了 unchecked_cast、saturating_cast、truncating_cast、exact_cast。

    修复后的规则变成:

    • 纯 builtin numeric pair 只走 std_numeric 路径
    • 只有涉及自定义 underlying 类型时,才走 underlying bridge 路径

    这样一来,builtin-to-builtin 的调用不再落入两套公开候选,编译器也就不需要再做高成本的约束排序。

    2.3 为什么块2 / 块3会把问题放大

    • primitive.impl 中的 convert_underlying_() 是跨 underlying 构造、赋值、store、compare_exchange 的公共入口,属于高扇出热点。
    • operations.operators 中的 apply_assign() 是 +=、-=、*=、/=、位运算赋值等一整组复合赋值的公共入口,也是高扇出热点。

    因此,只要这两个入口重新直接调用 conversion::numeric_risk / conversion::saturating_cast,上面那组重叠约束带来的编译成本就会被全局放大。

    3. 后续验证结果

    • builtin 类型之所以会同时满足 std_numeric 与 underlying_type,可以直接从以下代码看到:

    template <mcpplibs::primitives::std_underlying_type T>
    struct mcpplibs::primitives::underlying::traits<T> {
    using value_type = std::remove_cv_t<T>;
    using rep_type = value_type;
    static constexpr bool enabled = true;

    concept std_numeric = std_integer<T> || std_floating<T>;

    concept underlying_type =
    underlying::details::enabled<T> && underlying::details::has_category<T> &&
    underlying::details::has_rep_bridge<T> &&
    underlying::details::has_supported_rep_type<T> &&
    underlying::details::has_consistent_category<T>;
    template <typename T>
    concept underlying_operand = underlying_type<std::remove_cvref_t<T>>;
    template <typename T>
    concept boolean_underlying_type =
    underlying_type<T> && (underlying::traits<std::remove_cv_t<T>>::kind ==
    underlying::category::boolean);
    template <typename T>
    concept character_underlying_type =
    underlying_type<T> && (underlying::traits<std::remove_cv_t<T>>::kind ==
    underlying::category::character);
    template <typename T>
    concept integer_underlying_type =
    underlying_type<T> && (underlying::traits<std::remove_cv_t<T>>::kind ==
    underlying::category::integer);
    template <typename T>
    concept floating_underlying_type =
    underlying_type<T> && (underlying::traits<std::remove_cv_t<T>>::kind ==
    underlying::category::floating);
    template <typename T>
    concept numeric_underlying_type =
    integer_underlying_type<T> || floating_underlying_type<T>;

    • 根因修复位于:

    template <std_numeric DestRep, std_numeric SrcRep>
    requires details::statically_castable<DestRep, SrcRep>
    constexpr auto unchecked_cast(SrcRep value) noexcept
    -> std::remove_cvref_t<DestRep> {
    return details::unchecked_rep_cast<DestRep>(value);
    }
    template <std_numeric DestRep, std_numeric SrcRep>
    requires details::statically_castable<DestRep, SrcRep>
    constexpr auto checked_cast(SrcRep value)
    -> cast_result<std::remove_cvref_t<DestRep>> {
    return details::checked_rep_cast<DestRep>(value);
    }
    template <std_numeric DestRep, std_numeric SrcRep>
    requires details::statically_castable<DestRep, SrcRep>
    constexpr auto saturating_cast(SrcRep value) noexcept
    -> std::remove_cvref_t<DestRep> {
    return details::saturating_rep_cast<DestRep>(value);
    }
    template <std_numeric DestRep, std_numeric SrcRep>
    requires details::statically_castable<DestRep, SrcRep>
    constexpr auto truncating_cast(SrcRep value) noexcept
    -> std::remove_cvref_t<DestRep> {
    return details::truncating_rep_cast<DestRep>(value);
    }
    template <std_numeric DestRep, std_numeric SrcRep>
    requires details::statically_castable<DestRep, SrcRep>
    constexpr auto exact_cast(SrcRep value)
    -> cast_result<std::remove_cvref_t<DestRep>> {
    return details::exact_rep_cast<DestRep>(value);
    }
    template <underlying_type Dest, underlying_type Src>
    requires(!details::builtin_numeric_pair<std::remove_cv_t<Dest>,
    std::remove_cv_t<Src>>)
    constexpr auto unchecked_cast(Src value) noexcept -> Dest {
    return details::cast_underlying_value<Dest>(
    value, []<typename DestRep, typename SrcRep>(SrcRep rep) {
    return details::unchecked_rep_cast<DestRep>(rep);
    });
    }
    template <underlying_type Dest, underlying_type Src>
    requires(!details::builtin_numeric_pair<std::remove_cv_t<Dest>,
    std::remove_cv_t<Src>>)
    constexpr auto checked_cast(Src value) -> cast_result<Dest> {
    return details::cast_underlying_result<Dest>(
    value, []<typename DestRep, typename SrcRep>(SrcRep rep) {
    return details::checked_rep_cast<DestRep>(rep);
    });
    }
    template <underlying_type Dest, underlying_type Src>
    requires(!details::builtin_numeric_pair<std::remove_cv_t<Dest>,
    std::remove_cv_t<Src>>)
    constexpr auto saturating_cast(Src value) noexcept -> Dest {
    return details::cast_underlying_value<Dest>(
    value, []<typename DestRep, typename SrcRep>(SrcRep rep) {
    return details::saturating_rep_cast<DestRep>(rep);
    });
    }
    template <underlying_type Dest, underlying_type Src>
    requires(!details::builtin_numeric_pair<std::remove_cv_t<Dest>,
    std::remove_cv_t<Src>>)
    constexpr auto truncating_cast(Src value) noexcept -> Dest {
    return details::cast_underlying_value<Dest>(
    value, []<typename DestRep, typename SrcRep>(SrcRep rep) {
    return details::truncating_rep_cast<DestRep>(rep);
    });
    }
    template <underlying_type Dest, underlying_type Src>
    requires(!details::builtin_numeric_pair<std::remove_cv_t<Dest>,
    std::remove_cv_t<Src>>)
    constexpr auto exact_cast(Src value) -> cast_result<Dest> {

    这里将公开 cast API 的 builtin 路径收紧为 std_numeric,并显式排除了 builtin-to-builtin 情况下走 underlying_type bridge 的可能性。

    • 修复后,先前为规避超时而临时回退的两处逻辑已经重新接回公共 conversion:

    static constexpr auto convert_underlying_(Source source) noexcept -> Target {
    using source_value_type = std::remove_cv_t<Source>;
    using source_rep_type = underlying::traits<source_value_type>::rep_type;
    using target_rep_type =
    underlying::traits<std::remove_cv_t<Target>>::rep_type;
    auto const source_rep = underlying::traits<source_value_type>::to_rep(source);
    auto const target_rep =
    conversion::saturating_cast<target_rep_type>(
    static_cast<source_rep_type>(source_rep));
    return underlying::traits<std::remove_cv_t<Target>>::from_rep(target_rep);

    constexpr auto apply_assign(Lhs &lhs, Rhs const &rhs)
    -> primitive_dispatch_result_t<OpTag, Lhs, Rhs, ErrorPayload> {
    using lhs_traits = meta::traits<Lhs>;
    using lhs_value_type = lhs_traits::value_type;
    using lhs_value_policy = lhs_traits::value_policy;
    using lhs_rep = underlying::traits<lhs_value_type>::rep_type;
    using assign_meta = dispatcher_meta<OpTag, Lhs, Rhs, ErrorPayload>;
    using common_value_type = assign_meta::common_rep;
    using common_rep = underlying::traits<common_value_type>::rep_type;
    auto out = apply<OpTag, Lhs, Rhs, ErrorPayload>(lhs, rhs);
    if (!out.has_value()) {
    return std::unexpected(out.error());
    }
    auto const assigned_common = out->load();
    auto const assigned_common_rep =
    underlying::traits<common_value_type>::to_rep(assigned_common);
    if constexpr (std::same_as<lhs_value_policy, policy::value::checked> &&
    std_integer<lhs_rep> && std_numeric<common_rep>) {
    if (auto const kind =
    conversion::numeric_risk<lhs_rep>(assigned_common_rep);
    kind.has_value()) {
    return std::unexpected(
    details::to_error_payload<ErrorPayload>(
    details::to_policy_error_kind(*kind)));
    }
    }
    auto const assigned_rep =
    conversion::saturating_cast<lhs_rep>(assigned_common_rep);
    lhs.store(underlying::traits<lhs_value_type>::from_rep(assigned_rep));

    • 对应的 MSVC conversion 用例也已恢复为真实断言,不再使用 _MSC_VER 下的 smoke 分支:

    TEST(ConversionCastTest, CheckedCastReportsErrorForInvalidInput) {
    auto const ok = conversion::checked_cast<int>(42u);
    ASSERT_TRUE(ok.has_value());
    EXPECT_EQ(*ok, 42);
    auto const bad = conversion::checked_cast<std::uint16_t>(-7);
    ASSERT_FALSE(bad.has_value());
    EXPECT_EQ(bad.error(), conversion::risk::kind::underflow);

    TEST(ConversionCastTest, SaturatingCastClampsAndHandlesNaN) {
    EXPECT_EQ(conversion::saturating_cast<std::int16_t>(100000),
    std::numeric_limits<std::int16_t>::max());
    EXPECT_EQ(conversion::saturating_cast<int>(
    std::numeric_limits<double>::quiet_NaN()),
    0);

    • 验证结果如下:

      • cmake + clang 下,先前会卡住的 basic_conversion_tests 已可正常构建并通过。
      • xmake + msvc 下,块2单独还原、块3单独还原、块2+块3同时还原,均可在 120 秒限制内完成构建并通过全部测试。
      • 在恢复 MSVC 下的真实 conversion 断言后,Windows 本地完整测试仍然全部通过。
      • PR Enhance type classification and simplify numeric conversion functions #16 最新推送后,Linux / macOS / Windows 三个平台 CI 均已通过。
  3. added a commit that references this issue on Mar 26, 2026
    109fdef
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions