Repository navigation
PR#16 compiles extremely slowly in the Windows MSVC environment #17
Description
Activity
- added 2 commits that reference this issue
on Mar 26, 2026 排查报告
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 问题版本:公开
castAPI 的候选集重叠旧版本中,公开
castAPI 同时存在两套可见重载: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,可以直接从以下代码看到:
primitives/src/underlying/impl.cppm
Lines 9 to 14 in be07d3f
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;
primitives/src/underlying/traits.cppm
Line 29 in be07d3f
concept std_numeric = std_integer<T> || std_floating<T>;
primitives/src/underlying/traits.cppm
Lines 167 to 198 in be07d3f
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>; - 根因修复位于:
primitives/src/conversion/underlying.cppm
Lines 306 to 384 in 2452fba
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> { 这里将公开
castAPI 的 builtin 路径收紧为std_numeric,并显式排除了 builtin-to-builtin 情况下走underlying_typebridge 的可能性。- 修复后,先前为规避超时而临时回退的两处逻辑已经重新接回公共
conversion:
primitives/src/primitive/impl.cppm
Lines 59 to 69 in be07d3f
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);
primitives/src/operations/operators.cppm
Lines 529 to 560 in be07d3f
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 分支:
primitives/tests/basic/conversion/underlying/test_builtin_casts.cpp
Lines 11 to 18 in 07c4816
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 均已通过。
- 项目通过
- added a commit that references this issue
on Mar 26, 2026
问题描述
在
mcpplibs/primitives的 PR #16(feat-explicit-conversion)中,Windows CI 明显慢于 Linux/macOS,且测试目标构建阶段出现超时/取消。影响
观测到的现象
GitHub Actions(PR #16)
23537726928:build-windows约 20 分钟后 cancelledbuild-linux~1 分钟失败build-macos~2 分钟成功23540994438:build-windows约 30 分钟后 cancelledbuild-linux~2 分钟成功build-macos~2 分钟成功primitives_test构建阶段(Build and teststep)可能原因(待确认)
-j2复现步骤
GitHub Actions 复现
Build and test阶段长时间运行后 cancelled。期望行为
primitives_test能在超时前稳定完成。建议的下一步
$env:NUMBER_OF_PROCESSORS),并减少重复分段构建。比较 Windows
primitives_test纯构建耗时与总 job 耗时。相关信息
1825132