C++ 中文周刊 2025-04-13 第181期

周刊项目地址

公众号

点击「查看原文」跳转到 GitHub 上对应文件,链接就可以点击了

qq群 点击进入 满了加这个 729240657

RSS

欢迎投稿,推荐或自荐文章/软件/资源等,评论区留言


资讯

标准委员会动态/ide/编译器信息放在这里

编译器信息最新动态推荐关注hellogcc公众号 本周更新 2025-01-08 第288期

性能周刊

文章

Announcing Guidelines Support Library v4.2.0

主要改动就是gsl span性能提升,以前相比std::span很慢(边界检查)

How can I choose a different C++ constructor at runtime?

考虑我们想要调用不同的基类

看代码

struct WidgetBase
{
    // local mode
    WidgetBase();

    // remote mode
    WidgetBase(std::string const& server);

    // The mutex makes this non-copyable, non-movable
    std::mutex m_mutex;
};

struct WidgetOptions
{
     random stuff 
};

struct Widget : WidgetBase
{
    Widget(WidgetOptions const& options) :
        // 不好使                
        CanBeLocal(options)                 
            ? WidgetBase()                  
            : WidgetBase(GetServer(options))
    {}

    static bool CanBeLocal(WidgetOptions const&);
    static std::string GetServer(WidgetOptions const&);
};

怎么搞?通过create函数+返回值优化来转发

struct Widget : WidgetBase
{
    Widget(WidgetOptions const& options) :
        WidgetBase(ChooseWidgetBase(options))
    {}

    static bool CanBeLocal(WidgetOptions const&);
    static std::string GetServer(WidgetOptions const&);

private:
    static WidgetBase ChooseWidgetBase(           
        WidgetOptions const& options)             
    {                                             
        if (CanBeLocal(options)) {                
            return WidgetBase();                  
        } else {                                  
            return WidgetBase(GetServer(options));
        }                                         
    }                                             
};

Troubleshooting between C++ Module and NVCC

分享一下失敗的經驗, 還想不到什麼好方法讓Module跟非Module溝通..

目前不推荐使用module

Improving on std::count_if()’s auto-vectorization

他的场景是这样的,检查一组uint8数组判断偶数个数,并且已经确认偶数在0-255之间

简单代码示这样的

auto count_even_values_v1(const std::vector<uint8_t> &vec)
{
    return std::count_if(
        vec.begin(),
        vec.end(),
        [](uint8_t x) { return x % 2 == 0; }
    );
}

观察汇编

.LCPI0_1:
    .byte   1
count_even_values_v1():
;   ............
    vpbroadcastb    xmm1, byte ptr [rip + .LCPI0_1]
.LBB0_6:
    vmovd   xmm2, dword ptr [rsi + rax]
    vpandn  xmm2, xmm2, xmm1
    vpmovzxbq       ymm2, xmm2
    vpaddq  ymm0, ymm0, ymm2
    add     rax, 4
    cmp     r8, rax
    jne     .LBB0_6

不知道为什么扩展成64位了。

原因在于countif的返回值difference_type,

但我们结果明显小于255,可以改成uint8

所以代码改成这样

template<typename Acc, typename It, typename Pred>
Acc custom_count_if(It begin, It end, Pred pred)
{
    Acc result = 0;
    for (auto it = begin; it != end; it++)
    {
        if (pred(*it))
            result++;
    }
    return result;
}

auto count_even_values_v2(const std::vector<uint8_t> &vec)
{
    return custom_count_if<uint8_t>(
        vec.begin(),
        vec.end(),
        [](uint8_t x) { return x % 2 == 0; }
    );
}

再看汇编



.LCPI0_2:
    .byte   1
count_even_values_v2():
;   ............
    vpbroadcastb    ymm1, byte ptr [rip + .LCPI0_2]
.LBB0_11:
    vmovdqu ymm2, ymmword ptr [rdx + rax]
    vpandn  ymm2, ymm2, ymm1
    vpaddb  ymm0, ymm2, ymm0
    add     rax, 32
    cmp     rdi, rax
    jne     .LBB0_11

可以看到之前的汇编是dword

每次处理4个8位值(dword),通过vpandn提取最低位并取反,再通过vpmovzxbq将结果零扩展为64位累加

现在是ymmword

每次处理32个8位值(ymmword),使用vpaddb直接累加8位结果

测试显示速度快两倍以上 godbolt https://godbolt.org/z/vvfeTsfKx

这个例子让我想起之前聊过的64*64计算问题

如果计算64 x 64 -> 128 不要提前转128 计算途中转就可以,编译器知道你想干嘛,提前转128会导致编译器认为你想生成256的数去截断 128,导致多余的mul

https://github.com/StarRocks/starrocks/pull/11966

他这个优化就是先写出坑的代码,再改汇编改进

能确定结果集的优化会更立竿见影

另外修改返回值也能获得这个优化,感谢LH_mouse/萧叶轩指出

通过调整布局跳过序列化反序列化

如果压缩就最外层zstd/lz4 不考虑字段兼容性问题,只为了快

游戏场景pb接受不了的场景

https://github.com/CppCon/CppCon2024/blob/main/Presentations/Using_Modern_Cpp_to_Build_XOffsetDatastructure.pdf

https://github.com/ximicpp/XOffsetDatastructure

https://www.youtube.com/watch?v=agRbVcMkqTY

看一乐

Advanced C++ Optimization Techniques for High-Performance Applications — Part 1

讲了分支预测like/unlike,缓存优化,SIMD

缓存优化讲了

一个loop tiling举例

const int BLOCK = 1024;
for (int start = 0; start < N; start += BLOCK) {
    int end = std::min(start + BLOCK, N);
    for (int i = start; i < end; ++i) {
        result[i] += compute(A[i], B[i]);
    }
}

Advanced C++ Optimization Techniques for High-Performance Applications — Part 2

讲了循环展开/向量化,函数内联和指令缓存效应,返回值优化,链接优化(LTO/WPO)内存对齐/填充,SOA/AOS

其他的没啥说的,讲过多次

Advanced C++ Optimization Techniques for High-Performance Applications — Part 3

讲了无锁编程,线程亲和,numa

false sharing问题,对象在同一个内存行导致互相影响

比如

struct PaddedCounters {
    alignas(64) std::atomic<int> c1;
    alignas(64) std::atomic<int> c2;
};

其他没啥有用的东西

Performance Engineering — Part 1

检查过多的上下文切换,如何判定

缓解

检测内存碎片

缓解

Polymorphic, Defaulted Equality

案发现场

struct Base {
    virtual ~Base() = default;
    virtual auto operator==(Base const&) const -> bool = 0;
};

struct Derived : Base {
    int m1, m2;
    bool operator==(Base const& rhs) const override {
        if (typeid(rhs) == typeid(Derived)) {  // 确保类型严格匹配
            return *this == static_cast<Derived const&>(rhs); // 调用默认比较
        }
        return false;
    }
    bool operator==(Derived const&) const = default;  // 默认生成
};

默认生成会生成

auto Derived::operator==(Derived const& rhs) const -> bool {
    return static_cast<Base const&>(*this) == static_cast<Base const&>(rhs)
       and m1 == rhs.m1
       and m2 == rhs.m2;
}

调用基类,基类再调用子类,无限循环

解决方法就是这种接口(operator ==)别虚函数

Bypassing the branch predictor

考虑这么个场景

struct Transaction;
bool should_send(Transaction *t);
void send(Transaction *t);
void abandon(Transaction *t);

void resolve(Transaction *t)
{
    if (should_send(t)) {
        send(t);
    } else {
        abandon(t);
    }
}

在金融交易系统中,大部分交易请求会被放弃(abandon()),仅有少数需要发送(send())。由于分支预测器倾向于预测进入高频路径(abandon()),当实际需要执行低频的 send() 时,会导致以下性能问题

这种场景是likely这种东西无法搞定的,需要的是硬件写死的那种需求

不是说likely不行,你加上likely没法改变运行状态中分支预测器的行为,只是改变汇编

有一种玩法就是cache warm,长时间跑假的执行的数据骗他。不过CPU肯定上升就是了

开源项目介绍


上一期

本期

下一期

看到这里或许你有建议或者疑问或者指出错误,请留言评论! 多谢! 你的评论非常重要!也可以帮忙点赞收藏转发!多谢支持! 觉得写的不错那就给点吧, 在线乞讨 微信转账