C++ 中文周刊 2025-07-13 第187期

周刊项目地址

公众号

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

qq群 753792291 答疑在这里

RSS

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

熬夜看了EWC饿狼传说比赛,小孩曾卓君亚军,被那个卡恩压的动不了

比街霸6爱德煤球还离谱的无责任压制,气的睡不着,傻逼游戏难怪销量不行


资讯

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

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

性能周刊

上一期补充

qt支持告警 clazy warning: “c++11 range-loop might detach Qt container ,自动优化cow forrange问题

文章

Base64 for compression

非utf-8字符 比如中文字符串会转成八进制字符,膨胀率非常高

如果需要降低体积,可以考虑使用base64压缩,gcc15支持

.base64 "w6nDqcOpw6nDqcOpw6nDqQA="

引入计算,降低体积,大家看需求取舍

Discover C++26’s compile-time reflection

演示了一个生成sql的代码

template<typename T>
consteval std::string generate_sql_columns() {
    std::string columns;
    bool first = true;

    constexpr auto ctx = std::meta::access_context::current();

    [:expand(std::meta::nonstatic_data_members_of(^^T, ctx)):] 
                        >> [&]<auto member>{
            using member_type = typename[:type_of(member):];
            if (!first) {
                columns += ", ";
            }
            first = false;

            // Get member name
            auto name = std::meta::identifier_of(member);
            columns += name;
    };

    return columns;
}
template<typename T>
constexpr std::string generate_sql_valuess(const T& obj) {
    std::string values;
    bool first = true;

    constexpr auto ctx = std::meta::access_context::current();

    [:expand(std::meta::nonstatic_data_members_of(^^T, ctx)):] 
                        >> [&]<auto member>{
        using member_type = typename[:type_of(member):];
        if (!first) {
            values += ", ";
        }
        first = false;

        // Get member value
        auto value = obj.[:member:];

        // Format value based on type
        if constexpr (std::same_as<member_type, std::string>) {
            // Escape single quotes in strings
            std::string escaped = value;
            size_t pos = 0;
            while ((pos = escaped.find('\'', pos)) 
                      != std::string::npos) {
                escaped.replace(pos, 1, "''");
                pos += 2;
            }
            values += "'" + escaped + "'";
        } else if constexpr (std::is_arithmetic_v<member_type>) {
            values += std::to_string(value);
        }
    };

    return values;
}
template<typename T>
constexpr std::string generate_sql_insert(const T& obj, const std::string& table_name) {
    constexpr std::string columns = generate_sql_columns<T>();
    std::string values = generate_sql_valuess(obj);
    return "INSERT INTO " + table_name 
          + " (" + columns + ") VALUES (" + values + ");";
}

我之前用protobuf生成过sql,代码类似这样

template <class PB>
std::string ProtoToUpdateSql(PB pb, const std::string& table, const std::string& whereCondition) {
  std::vector<std::string> updateFields;

  // 使用反射获取所有字段
  const google::protobuf::Descriptor* descriptor = pb.GetDescriptor();
  const google::protobuf::Reflection* reflection = pb.GetReflection();

  for (int i = 0; i < descriptor->field_count(); i++) {
    const google::protobuf::FieldDescriptor* field = descriptor->field(i);

    // 跳过没有设置值的字段
    if (!reflection->HasField(pb, field)) {
      continue;
    }

    std::string fieldName = field->name();
    std::string value;

    // 根据字段类型获取值
    switch (field->cpp_type()) {
      case google::protobuf::FieldDescriptor::CPPTYPE_INT32:
        value = std::to_string(reflection->GetInt32(pb, field));
        break;
      case google::protobuf::FieldDescriptor::CPPTYPE_INT64:
        value = std::to_string(reflection->GetInt64(pb, field));
        break;
      case google::protobuf::FieldDescriptor::CPPTYPE_UINT64:
        value = std::to_string(reflection->GetUInt64(pb, field));
        break;
      case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE:
        value = std::to_string(reflection->GetDouble(pb, field));
        break;
      case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT:
        value = std::to_string(reflection->GetFloat(pb, field));
        break;
      case google::protobuf::FieldDescriptor::CPPTYPE_BOOL:
        value = reflection->GetBool(pb, field) ? "TRUE" : "FALSE";
        break;
      case google::protobuf::FieldDescriptor::CPPTYPE_ENUM:
        value = std::to_string(reflection->GetEnumValue(pb, field));
        break;
      default:
        value = "'" + ExtraString(reflection->GetString(pb, field)) + "'";
        break;
    }

    updateFields.push_back(fieldName + " = " + value);
  }

  // 如果没有字段需要更新,返回空字符串
  if (updateFields.empty()) {
    return "";
  }

  // 构建SQL语句
  std::stringstream ss;
  ss << "UPDATE " << table << " SET ";

  for (size_t i = 0; i < updateFields.size(); i++) {
    if (i > 0)
      ss << ", ";
    ss << updateFields[i];
  }

  ss << " WHERE " << whereCondition;

  return ss.str();
}

大家可以比较一下差距

CUDA优化黑魔法:假装CUTLASS库

很简单的一个黑魔法,只要在你的函数名前加上cutlass_,假装是CUTLASS库,有可能获得一定的性能提升

有点夸张,来源

Advanced NVIDIA CUDA Kernel Optimization Techniques: Handwritten PTX

手写PTX性能提升7%-10%,怎么写?

文章后面展示了一个GEMM plus top_k and softmax的例子,不懂,就不贴了

Case study of over-engineered C++ code

代码走读一下

群友投稿, string奇怪的比较表现

#include <iostream>
#include <string>

int main() {
    std::string a;
    std::string b;
    a.push_back(64);
    b.push_back(-28);
    char c = 64;
    char d = -28;
    std::cout << (a < b) << std::endl; // output 1
    std::cout << (c < d) << std::endl; // output 0
    return 0;
}

原因 string内部char trait cmp使用unsigned char进行比较

有点像strcmp,不知道是不是为了兼容这个逻辑

gcc已经实现编译期异常

感谢mm投稿,非常好的功能 异常开销越来越小

Once more about dynamic_cast, a real use case

发布版本升级动态库以来dynamic_cast进行版本升级

代码类似

// https://godbolt.org/z/TK9WM65n1
#include <iostream>
#include <memory>

// sdk

class InterfaceForSomeServiceBase{
public:
    virtual ~InterfaceForSomeServiceBase() = default;
};

class InterfaceForSomeService_v1: public InterfaceForSomeServiceBase {
public:
	virtual void featureA() = 0;
	virtual void featureB() = 0;
};

class InterfaceForSomeService_v2 : public InterfaceForSomeService_v1 {
public:
	virtual void featureC() = 0;
};

class InterfaceForSomeService_v3 : public InterfaceForSomeService_v2 {
public:
	virtual void featureD() = 0;
};


class Server {
public:
    void handle(std::unique_ptr<InterfaceForSomeServiceBase> clientImplementation) {
        if (auto* p = dynamic_cast<InterfaceForSomeService_v3*>(clientImplementation.get()); p) {
            p->featureA();
            p->featureB();
            p->featureC();
            p->featureD();
        } else if (auto* p = dynamic_cast<InterfaceForSomeService_v2*>(clientImplementation.get()); p) {
            p->featureA();
            p->featureB();
            p->featureC();
        } else if (auto* p = dynamic_cast<InterfaceForSomeService_v1*>(clientImplementation.get()); p) {
            p->featureA();
            p->featureB();
        } else {
            std::cout << "unhandled version\n";
        }
    }
};


// client

class ClientServiceImplementation : public InterfaceForSomeService_v2 {
public:
	void featureA() override {
        std::cout << "ClientServiceImplementation(InterfaceForSomeService_v2)::featureA\n";
    }
	void featureB() override {
        std::cout << "ClientServiceImplementation(InterfaceForSomeService_v2)::featureB\n";
    }
    void featureC() override {
        std::cout << "ClientServiceImplementation(InterfaceForSomeService_v2)::featureC\n";
    }
};

// server

std::unique_ptr<InterfaceForSomeServiceBase> LoadFromDLL() {
    return std::make_unique<ClientServiceImplementation>();
}

int main() {
    std::unique_ptr<InterfaceForSomeServiceBase> clientImplementation = LoadFromDLL();

    Server s;
    s.handle(std::move(clientImplementation));
}

不过这么玩的越来越少了

首先是二进制边界传染

为了规避这种传染问题,通过socket来搞,然后用version字段解决

60 Second Lesson: C++ Token Pasting Metaprogramming

真能编词儿

就是通过字符串注册模版来统一接口,类似static 工厂模式

看代码

#include <iostream>
#define CONCAT(A, B) A ## B

int FooFunctionOne() {
    return 1;
}

int BarFunctionOne() {
    return -1;
}
template<int (*Func)()>
class UsesTemplates {
public:
    int CallFunction() {
        return Func();                              
    }
};

//  Example:  TEMPLATE_CLASS(Foo) -> UsesTemplates<&FooFunctionOne>
#define TEMPLATE_CLASS(PREFIX) UsesTemplates<&CONCAT(PREFIX, FunctionOne)>

int main() {
    TEMPLATE_CLASS(Foo) fooClass;   // Expands to UsesTemplates<&FooFunctionOne>
    TEMPLATE_CLASS(Bar) barClass;   // Expands to UsesTemplates<&BarFunctionOne>

    const auto fooValue { fooClass.CallFunction() };
    const auto barValue { barClass.CallFunction() };

    std::cout << "Foo = " << fooValue << '\n';
    std::cout << "Bar = " << barValue << '\n';
    return 0;
}

真能编词儿

群友Da’Inihlus评注: AI编的

Just say no to broken JSON

不要兼容不合法的json,比如没转义之类的特殊场景json,提高复杂度技术债,越简单越好

Fail Case: Polymorphism Without virtual in C++: Concepts, Traits, and Ref

抄了个fat pointer类似实现,但性能还不如虚函数,内联优化很差

### The case of the invalid handle error when a handle is closed while a thread is waiting on it

代码现场类似这样

struct Widget : std::enable_shared_from_this<Widget>
{
    wil::unique_event m_readyEvent{ wil::EventOptions::ManualReset };
    wil::unique_event m_shutdownEvent{ wil::EventOptions::ManualReset };

    winrt::IAsyncOperation<bool> WaitUntilReadyAsync()
    {
        co_await winrt::resume_background();

        HANDLE events[] = { m_readyEvent.get(), m_shutdownEvent.get() };
        auto status = WaitForMultipleObjects(ARRAYSIZE(events), events,
                        FALSE /* bWaitAll */, INFINITE);

        switch (status) {
        case WAIT_OBJECT_0:
            co_return true; // the ready event is set

        case WAIT_OBJECT_0 + 1:
            co_return false; // the shutdown event is set

        case WAIT_FAILED:
            FAIL_FAST_LAST_ERROR();

        default:
            FAIL_FAST();
        }
    }
};

存在一个线程 WaitForMultipleObjects另一个线程Close的场景

解决办法就是在wait中延长生命

winrt::IAsyncOperation<bool> WaitUntilReadyAsync(){
    auto lifetime = shared_from_this();

    co_await winrt::resume_background();

    // ⟦ ... rest as before ... ⟧
}

类似场景我记得讲过很多次,搜了一下就一次,怪了

C++26: std::format improvement Part 1

修复to_string的坑,以前的实现使用sprintf

std::cout << std::to_string(-1e-7);  // prints: -0.000000
std::cout << std::to_string(0.42); // prints: 0.420000

c++26可以使用fmt挽救,加上to_chars结果终于正确了 gcc14可用

fmt参数不匹配的运行崩溃代价过大

比如

std::format("{:>{}}", "hello", "10"); // Runtime error

其实使用fmtlib这种体验更明显,内部通过compile_parse_context检查匹配,标准库准备跟进,gcc15落地

格式化指针

#include <iostream> 
#include <format>

int main() { 
    auto ptr = new int(42);
    std::cout << &ptr << '\n';
    // std::cout << std::format("{:#018x}", ptr) << '\n'; // error in C++23
    std::cout << std::format("{:#018x}", reinterpret_cast<uintptr_t>(ptr)) << '\n';
    std::cout << std::format("{:#018X}", reinterpret_cast<uintptr_t>(ptr)) << '\n';

    delete ptr;
} 

新的编译器都实现了 gcc 15/ clang 18

成员visit

// Before:
auto format(S s, format_context& ctx) {
  int width = visit_format_arg([](auto value) -> int {
    if constexpr (!is_integral_v<decltype(value)>)
      throw format_error("width is not integral");
    else if (value < 0 || value > numeric_limits<int>::max())
      throw format_error("invalid width");
    else
      return value;
    }, ctx.arg(width_arg_id));
  return format_to(ctx.out(), "{0:x<{1}}", s.value, width);
}

// After:
auto format(S s, format_context& ctx) {
  int width = ctx.arg(width_arg_id).visit([](auto value) -> int {
    if constexpr (!is_integral_v<decltype(value)>)
      throw format_error("width is not integral");
    else if (value < 0 || value > numeric_limits<int>::max())
      throw format_error("invalid width");
    else
      return value;
    });
  return format_to(ctx.out(), "{0:x<{1}}", s.value, width);
}

代码更好看了

理解C和C++程序的编码

知乎 感谢 YexuanXiao投稿

Data-Parallel Types – A First Example

c++26simd 直接贴代码了

#include <experimental/simd>
#include <iostream>
#include <string_view>
namespace stdx = std::experimental;
 
void println(std::string_view name, auto const& a)
{
    std::cout << name << ": ";
    for (std::size_t i{}; i != std::size(a); ++i)
        std::cout << a[i] << ' ';
    std::cout << '\n';
}
 
template<class A>
stdx::simd<int, A> my_abs(stdx::simd<int, A> x)
{
    where(x < 0, x) = -x;
    return x;
}
 
int main()
{
    const stdx::native_simd<int> a = 1;
    println("a", a);
 
    const stdx::native_simd<int> b([](int i) { return i - 2; });
    println("b", b);
 
    const auto c = a + b;
    println("c", c);
 
    const auto d = my_abs(c);
    println("d", d);
 
    const auto e = d * d;
    println("e", e);
 
    const auto inner_product = stdx::reduce(e);
    std::cout << "inner product: " << inner_product << '\n';
 
    const stdx::fixed_size_simd<long double, 16> x([](int i) { return i; });
    println("x", x);
    println("cos²(x) + sin²(x)", stdx::pow(stdx::cos(x), 2) + stdx::pow(stdx::sin(x), 2));
}

native_simd有点类似highway 自动检测

This-pointing Classes

有这么一种类,保存this做操作,类似下面的

class OneIndirection {
    OneIndirection* ptr_ = this;  // 初始化为指向自身

public:
    void foo() {
        ptr_->foo_impl();  // 通过指针调用实现
    }

    void foo_impl() {
        // ... 具体实现
    }
};

当然ptr并不总是等于this,要是相等没几把意义,主要是为了实现一种代理/链表作用

好了,问题来了,怎么move

直觉来说,move构造和move赋值都应该是no-op,啥也不干,默认生成的move赋值不对

我们考虑一个更现实的例子

template <class T>
class static_circular_buffer {
    T buffer_[64];       // 嵌入式数组
    T* head_ = &buffer_[0];  // 头指针,初始指向数组起始位置

public:
    // ... 其他成员
};

这种场景下生成的默认move赋值函数 operator=(T&&) 的head_一定是错的,要重新计算偏移

head_ = buffer_ + (other.head_ - other.buffer_);  // 基于当前实例的buffer_重新计算偏移

再来一个例子

class Widget {
    std::function<void()> foo_fn_;  // 存储回调

public:
    void foo1();
    void foo2();

    Widget() {
        // 绑定当前实例的成员函数到回调
        foo_fn_ = [this]() { foo2(); };  // 捕获this
    }

    void foo() {
        foo_fn_();  // 调用回调
    }
};

这种如果非得分享,就要延长this的生命周期

前面正好有类似的例子,要继承enable_from_this且shared_from_this

或者就不让move,不存在shared场景

class Widget {
    Widget(Widget&&) noexcept = delete;             // 禁用移动构造
    Widget& operator=(Widget&&) noexcept = delete;  // 禁用移动赋值
    // ... 其他成员
};

Ready, Set, Redis

介绍boost.redis为了测试做的重构努力

原来的代码

struct exec_op {
    connection* conn;
    std::shared_ptr<detail::request_info> info; // a request + extra info
    asio::coroutine coro{}; // a coroutine polyfill that uses switch/case statements

    // Will be called by Asio until self.complete() is called
    template <class Self>
    void operator()(Self& self , system::error_code = {}, std::size_t = 0)
    {
        BOOST_ASIO_CORO_REENTER(coro)
        {
            // Check whether the user wants to wait for the connection to
            // be stablished.
            if (info->req->get_config().cancel_if_not_connected && !conn->state.is_open) {
                BOOST_ASIO_CORO_YIELD asio::async_immediate(self.get_io_executor(), std::move(self));
                return self.complete(error::not_connected, 0);
            }

            // Add the request to the queue
            conn->state.add_request(info);

            // Notify the writer task that there is a new request available
            conn->writer_timer.cancel();

            while (true) {
                // Wait for the request to complete. We will be notified using a channel
                BOOST_ASIO_CORO_YIELD info->channel.async_wait(std::move(self));

                // Are we done yet?
                if (info->is_done) {
                    self.complete(info->ec, info->bytes_read);
                    return;
                }

                // Check for cancellations
                if (self.get_cancellation_state().cancelled() != asio::cancellation_type_t::none) {
                    // We can only honor cancellations if the request hasn't been sent to the server
                    if (!info->request_sent()) {
                        conn->state.remove_request(info);
                        self.complete(asio::error::operation_aborted, 0);
                        return;
                    } else {
                        // Can't cancel, keep waiting
                    }
                }
            }
        }
    }
};

template <class CompletionToken, class Response>
auto connection::async_exec(const request& req, Response& res, CompletionToken&& token)
{
    return asio::async_compose<CompletionToken, void(system::error_code, std::size_t)>(
        exec_op{this, detail::make_info(req, res)},
        token,
        *this
    );
}

可以看到代码和asio混在一起,很难调。目标是改成fsm+回调函数模式,剥离asio逻辑,方便hook测试

更改后代码


// The finite state machine returns exec_action objects
// communicating what should be done next so the algorithm can progress
enum class exec_action_type
{
    notify_writer, // We should notify the writer task
    wait,          // We should wait for the channel to be notified
    immediate,     // We should invoke asio::async_immediate() to avoid recursion problems
    done,          // We're done and should call self.complete()
};

struct exec_action
{
    exec_action_type type;
    error_code ec;            // has meaning if type == exec_action_type::done
    std::size_t bytes_read{}; // has meaning if type == exec_action_type::done
};

// Contains all the algorithm logic. It is cheap to create and copy.
// It is conceptually similar to a coroutine.
class exec_fsm {
    asio::coroutine coro{};
    std::shared_ptr<detail::request_info> info; // a request + extra info
public:
    explicit exec_fsm(std::shared_ptr<detail::request_info> info) : info(std::move(info)) {}
    
    std::shared_ptr<detail::request_info> get_info() const { return info; }

    // To run the algorithm, run the resume() function until it returns exec_action_type::done.
    exec_action resume(
        connection_state& st, // Contains connection state, but not any I/O objects
        asio::cancellation_type_t cancel_state // The cancellation state of the composed operation
    )
    {
        BOOST_ASIO_REENTER(coro)
        {
            // Check whether the user wants to wait for the connection to
            // be stablished.
            if (info->req->get_config().cancel_if_not_connected && !st.is_open) {
                BOOST_ASIO_CORO_YIELD {exec_action_type::immediate};
                return {exec_action_type::done, error::not_connected, 0};
            }

            // Add the request to the queue
            st.add_request(info);

            // Notify the writer task that there is a new request available
            BOOST_ASIO_CORO_YIELD {exec_action_type::notify_writer};

            while (true) {
                // Wait for the request to complete. We will be notified using a channel
                BOOST_ASIO_CORO_YIELD {exec_action_type::wait};

                // Are we done yet?
                if (info->is_done) {
                    return {exec_action_type::done, info->ec, info->bytes_read};
                }

                // Check for cancellations
                if (cancel_state != asio::cancellation_type_t::none) {
                    // We can only honor cancellations if the request hasn't been sent to the server
                    if (!info->request_sent()) {
                        conn->state.remove_request(info);
                        return {exec_action_type::done, asio::error::operation_aborted};
                    } else {
                        // Can't cancel, keep waiting
                    }
                }
            }
        }
    }
};

struct exec_op {
    connection* conn;
    exec_fsm fsm;

    // Will be called by Asio until self.complete() is called
    template <class Self>
    void operator()(Self& self, system::error_code = {}, std::size_t = 0)
    {
        // Call the FSM
        auto action = fsm.resume(conn->state, self.get_cancellation_state().cancelled());

        // Apply the required action
        switch (action.type)
        {
            case exec_action_type::notify_writer:
                conn->writer_timer.cancel();
                (*this)(self); // This action doesn't involve a callback, so invoke ourselves again
                break;
            case exec_action_type::wait:
                fsm.get_info()->channel.async_wait(std::move(self));
                break;
            case exec_action_type::immediate:
                asio::async_immediate(self.get_io_executor(), std::move(self));
                break;
            case exec_action_type::done:
                self.complete(action.ec, action.bytes_read);
                break;
        }
    }
};

这样可以hook fsm来检测逻辑

Little adventure in pursuit of errors. The Battle for Wesnoth!

鉴赏神秘代码

inline double stod(std::string_view str) {
  trim_for_from_chars(str);
  double res;
  auto [ptr, ec] = utils::charconv::from_chars(str.data(), 
                                   str.data() + str.size(), res);

  if(ec == std::errc::invalid_argument) {
    throw std::invalid_argument("");                // <=
  } else  if(ec == std::errc::result_out_of_range) {
    throw std::out_of_range("");                    // <=
  }
  return res;
}

抛异常抛了个寂寞

std::string dsgettext (const char * domainname, const char *msgid)
{
  std::string msgval = dgettext (domainname, msgid);
  if (msgval == msgid) {
    const char* firsthat = std::strchr (msgid, '^');
    if (firsthat == nullptr)
      msgval = msgid;
    else
      msgval = firsthat + 1;
  }
  return msgval;
}

反复赋值,不知道要干嘛

void mp_create_game::sync_with_depcheck()
{
  ....
  auto& game_types_list = find_widget<menu_button>("game_types");
  game_types_list.set_value(
    std::distance(level_types_.begin(),
                  std::find_if(level_types_.begin(), 
                               level_types_.begin(), // <=
                               [&](const level_type_info& info)
                               {
                                 return info.first == new_level_index.first;
                               })));
  ....
}

经典迭代器传错,range还是有道理滴

class install_dependencies : public modal_dialog
{
public:
  explicit install_dependencies(const addons_list& addons)
    : modal_dialog(window_id()), addons_(addons)   // <=
  {}
....
private:
  virtual const std::string& window_id() const override;
....
}

神秘依赖 构造函数依赖虚函数,未定义行为了

double readonly_context_impl::power_projection(const map_location& loc, 
                                               const move_map& dstsrc) const
{
  map_location used_locs[6];
  int ratings[6];
  std::fill_n(ratings, 0, 6);   // <=
  int num_used_locs = 0;
....
}

神秘调用参数用错,rating可以直接初始化的,或者range

Performance of the Python 3.14 tail-call interpreter

llvm 19回归问题给了误判,以为性能提升显著,实际上1%-5%

The ITTAGE indirect branch predictor

这个文章有点意思,直接gpt翻译了

许多动态分支预测算法基于一个简单前提:维护某种形式的历史数据表,当需要预测分支时,查找“上一次”的结果并假设历史会重演。用C++风格的伪代码,我倾向于将其建模为:

struct BranchDetails {
  // 用于标识分支的信息
};

struct BranchHistory {
  // 存储的分支信息

  // 基于历史状态预测分支结果
  bool predict() const { /* ... */ };

  // 根据已确定的分支结果更新状态
  void update(bool taken) { /* ... */ };
};

// 我们维护一个从BranchDetails到BranchHistory的映射。由于是硬件固定大小的存储,实际存储的条目数有限。后文会讨论替换策略等细节。
using PredictorState = FixedSizeMap<BranchDetails, BranchHistory>;

void on_resolve_branch(PredictorState &pred, BranchDetails &branch, bool taken) {
  pred[branch].update(taken);
}

bool predict_branch(PredictorState &pred, BranchDetails &branch) {
  return pred[branch].predict();
}

那么,BranchDetails和BranchHistory的具体内容是什么?最简单的方案(早期CPU曾采用)是仅用分支地址标识分支——本质上,为程序文本中的每个分支指令单独跟踪状态——并为每个分支使用1位历史信息:

struct BranchDetails { uintptr_t addr; };
struct BranchHistory {
  bool taken_;

  bool predict() { return taken_; }
  void update(bool taken) { taken_ = taken; }
};

次简单的策略是用一个小计数器(仅2位!)替代单比特状态,以引入一定的“滞后性”。根据分支结果递增或递减计数器,用符号位作为预测依据。事实证明,大多数分支具有强“偏向性”(例如“跳转”率为10%或90%的情况比50%更常见),少量滞后性可避免因偶发异常而完全遗忘历史模式:

struct BranchHistory {
  int2_t counter_;

  bool predict() { return counter_ >= 0; }
  void update(bool taken) {
   saturating_increment(&counter_, taken ? 1 : -1); // 饱和递增(避免溢出)
  }
};

仅用PC索引分支虽然简单高效,但存在局限性。许多分支的行为依赖于动态数据,若想提升预测准确率,需要更细粒度的区分能力

由于分支预测器位于CPU前端,需要在指令实际执行(甚至完全解码)前就做出预测,因此无法利用太多其他CPU状态信息。但有一个“免费”的上下文可用:程序计数器历史和近期分支的历史——毕竟预测器需要参与生成这些信息。

因此,分支预测器可以维护一个固定大小的环形缓冲区,以滚动方式存储“分支历史”或“PC历史”,并用这些状态区分不同的分支。最简单的情况下,每个历史位记录前一个分支是否跳转(1表示跳转,0表示不跳转)。更复杂的预测器可能包含无条件分支信息,或记录前几个PC值的若干位

constexpr int history_length = ...; // 历史长度

struct BranchDetails {
  uintptr_t pc;
  bitarray<history_length> history; // 历史位数组
};

历史长度的取舍 应该使用多长的历史?越长越好吗?

更长的历史能捕捉更复杂的模式,对行为稳定的程序(稳态下)可能学习到更细致的模式。但更长的历史意味着:

表格需要更多空间来存储简单模式; 可能需要更长时间学习(因状态数增加,需逐一遇到才能掌握)。 举个简单函数的例子

bool logging_active;

void log(const char *msg) {
  if (logging_active) {
    printf("%s", msg);
  }
}

假设该函数未被内联,且被可执行文件中多个位置调用。若logging_active在运行时是静态或基本不变的,该分支的预测准确率应接近完美。但如果考虑分支历史,预测器将不再将该分支视为单一实体,而是需要为每个到达该分支的路径单独跟踪状态。最坏情况下,若存储k位历史,则可能需要为该分支分配2^k个表格条目!更糟糕的是,每个状态需单独遇到才能学习,无法从其他路径的经验中获益。

TAGE算法的核心思想 现在,我们已具备足够的背景知识,可概述TAGE预测器的设计。

TAGE与传统预测器类似,会跟踪分支历史,但不同之处在于它跟踪数百甚至数千位的历史,从而可能学习长距离的模式。

为了在不导致状态爆炸的前提下利用这些历史,TAGE维护多张预测表(约10-20张),每张表的索引基于按几何级数增长的历史长度序列(即第N张表的历史长度Ln≈L0·rⁿ,其中r为比例因子)。对于每个分支,TAGE尝试自适应选择最短的有效历史长度(及对应的表)进行预测。

在简单的预测器中,历史表“不关心”自己存储的是哪些键,仅通过键的部分比特直接索引。例如,仅用PC索引的预测器可能有一个大小为2ᵏ的计数器数组,用PC的低k位选择条目。若两个分支的PC模2ᵏ相同,它们会碰撞并共享同一表项,且无需检测或处理这种碰撞。这种设计使表格极高效,且在许多场景下是合理的权衡——毕竟我们已接受预测器可能出错,碰撞只是另一种错误原因;检测和处理碰撞需要更多硬件和存储,而这些资源可能更适合用于降低其他类型的错误率。

但TAGE维护多张表,需要为不同分支分配不同表,因此必须知道哪些表项存储了目标分支的信息(而非碰撞的分支)。因此,除其他元数据外,每个表项还存储一个“标签”(tag)——额外的元数据位,用于标识该表项对应的分支键。

给定(PC,分支历史)元组T,TAGE对每张表使用两个哈希函数H_index和H_tag:分支状态T将存储在第N张表的H_index(T)索引处,标签为H_tag(T)。查找时,检查H_index(T)处的条目,并比较标签与H_tag(T)。

若标签不匹配,说明该条目存储的是其他分支的信息,不使用(但可能选择覆盖); 若标签匹配,假设该状态与当前分支匹配,使用或更新该条目(注意:仍基于哈希匹配,可能存在两个哈希均碰撞的不同分支,但通过设计哈希函数和表大小,这种情况在实际中极少)。 这些标签位赋予了TAGE中的“TA”(Tagged)之名;“GE”则源于历史长度的几何级数序列。

基础预测算法 基于上述设计,TAGE的基础预测算法非常简单。每个表项存储一个计数器(论文中称为ctr)——如前所述,分支跳转时递增,不跳转时递减。

预测时,TAGE遍历所有表,使用对应的历史长度。对于所有标签匹配的表项,选择历史长度最长的那个表项的预测结果。

基础预测器(最简情况下仅用PC索引的表)不使用标签位,因此总会匹配,若更长历史的表项均未命中,则作为备用。

预测错误时转向更长历史 一旦分支结果确定(已知正确结果),需更新预测器。

TAGE总会更新被选中的表项的ctr字段。但如果预测错误,它会尝试在更长历史的表中分配新条目。目标是动态尝试越来越长的历史,直到找到有效的预测模式。

跟踪表项的效用 由于表项带有标签,需决定何时替换旧条目、为新分支腾出空间。为使预测器高效,目标是保留未来可能产生有用预测的条目,淘汰无用的条目。

为此,TAGE跟踪每个条目是否“最近有用”。除标签和计数器外,每个表项还有一个“u”(useful)计数器(通常仅1或2位),记录该条目是否近期产生了有用预测。

分配新条目时(如前所述),仅会覆盖u=0的槽位,且新槽位初始化为u=0;因此,新条目必须证明自身价值,否则可能被替换。

u计数器的递增条件是:

该表项被用于预测; 预测结果正确; 该预测与下一长历史表项的预测结果不同。 因此,仅正确预测不足,还需该预测在“反事实”(即若未使用该条目)时会错误的情况下正确。

此外,u计数器会定期衰减(或置零),防止条目永久保留。具体算法在不同版本的论文中差异较大。

从TAGE到ITTAGE 前文描述的是TAGE的行为(预测条件分支的跳转/不跳转)。ITTAGE(本文主题,预测间接分支目标)的结构几乎相同,主要区别在于:

每个表项额外存储预测的目标地址; ctr计数器保留,但变为“置信度”计数器:预测正确时递增,错误时递减。仅在ctr为最小值时,才会用新值更新预测目标。因此,ctr跟踪对该目标地址的置信度,u计数器跟踪该条目在整体预测器中的效用。 事实上,这两种表可合并为一个联合预测器(论文中称为“COTTAGE”)。

关于TAGE和ITTAGE的公开资料不多,但以下是我找到的最有价值的链接(若你对细节感兴趣,建议阅读):

A case for (partially) tagged geometric history length branch prediction The L-TAGE Branch Predictor A 64 Kbytes ISL-TAGE branch predictor A 64-Kbytes ITTAGE indirect branch predictor The program for JWAC2, which hosted the CBP-3 competition

BOOM (Berkeley Out Of Order Machine)’s documentation on their TAGE implementation

我为何对ITTAGE感兴趣 一方面,我对ITTAGE感兴趣是因为偶尔需要思考解释器循环等软件的性能,它更新了我对这类场景的分析框架。具体来说,它影响了我上一篇文章中对CPython的基准测试分析。

另一方面,我对其更广泛的关联和应用场景感到着迷。

我曾写过一类软件工具(包括覆盖引导模糊测试器和追踪JIT),它们主要通过观察程序计数器(PC)的时间序列来理解程序行为,并在解释器等“状态隐藏于数据中、仅控制流不足以表征有趣状态”的程序上遇到挑战。

尽管未在之前的文章中明确提及,但我一直认为分支预测器属于这类工具——它们同样主要通过“PC序列”理解程序执行,且历史上在解释器循环上的表现也较差。因此,了解ITTAGE及其在解释器行为预测上的成功,自然引发了一个问题:能否从ITTAGE算法中借鉴经验,改进其他工具?

具体而言,我思考了以下几点:

ITTAGE能否用于覆盖引导模糊测试和程序状态探索? 如前所述,覆盖引导模糊测试通过生成候选输入,观察程序是否产生“新”行为来驱动探索。为使这一循环有效,需对程序行为进行某种表征或分桶,以区分“新”行为与“已观察”行为。

目前,这一目标通常通过“覆盖度”指标实现(统计PC值或分支的出现次数,本质是(PC,PC’)对)。这些计数器被分桶,执行的“指纹”由执行过程中生成的[(PC,分桶计数)]列表定义。

这种方法在实际中极其有效,但在某些程序(尤其是解释器)中可能失效——因其“有趣”状态无法很好地映射到PC集合或分支。我最爱的例证是IJON论文,它展示了具体问题并通过人工标注解决。

因此,我的问题是:类似TAGE/ITTAGE的方法能否帮助覆盖引导模糊测试器更好地探索解释器等程序的状态空间?例如,能否基于现有语料库训练TAGE类预测器,并根据预测误差率优先选择候选输入?这可能使模糊测试器仅通过注释解释器,即可有效探索解释型语言代码的状态空间。

尽管存在大量实际挑战,但理论上这可能实现更细腻的状态空间探索,发现仅通过长程相关性或模式才能识别的“新行为”。

需注意的是,TAGE/ITTAGE的设计针对硬件性能权衡优化,而软件的性能场景截然不同。因此,若此类方法可行,其细节可能与硬件版本差异较大(优化为高效的软件实现)。但核心思想——“为每个分支动态选择历史长度”——可能仍有借鉴价值。

更疯狂的想法是直接利用硬件分支预测器。现代CPU允许通过硬件性能计数器观察分支预测准确率,我们可以设想:用现有语料库训练分支预测器,通过观察硬件误预测次数作为“新颖性”信号。这一方法虽面临硬件分支预测器不透明、无法显式控制的挑战,但可能比软件预测器成本低得多。这让我好奇是否存在暴露分支预测器状态的CPU(例如提供“保存/恢复预测器状态”操作的接口),这将极大提升此类方法的可行性。

若你了解相关项目或受此启发想尝试,请务必告知我。

好奇心与强化学习 如前所述,我将TAGE/ITTAGE类算法应用于模糊测试的思路是:将“预测误差”作为奖励信号,重点关注高误差输入。

思考这一想法时,我意识到它似曾相识——从抽象层面看,这是强化学习领域的经典思路!

最著名的例子或许是2018年OpenAI发表的两篇“好奇心驱动学习”论文。这些研究探索了通过添加奖励项增强强化学习的方法——即使环境中无明确奖励信号,也能鼓励智能体探索。两篇论文的具体方法不同,但核心思想一致:除决定动作的策略网络外,训练一个预测网络,尝试预测环境特征或动作结果。策略模型会因发现高预测误差的动作或状态获得奖励——若一切顺利,这将鼓励智能体探索环境的新奇部分。

据我所知,这一技术效果显著;第二篇论文在《蒙特祖玛的复仇》(Montezuma’s Revenge)中达到了当时的顶尖水平。这款雅达利游戏对强化学习算法而言难度极高,因智能体需大量探索并操作钥匙和道具后才能获得分数。不过,我暂时不清楚该工作的后续发展。

我当时了解这些论文并关注相关研究,但在尝试将“ITTAGE”与“覆盖引导模糊测试”结合时,并未刻意联想到它们。这种思路的交汇让我觉得其中可能有潜力;不过与此同时,在2025年,或许直接用神经网络解决问题会比设计并调优定制预测算法更简单!

开源项目介绍

MSVC STL的deque实现的有问题导致非常慢,见#147

YexuanXiao投稿 他用C++20实现了一个性能不输libc++和libstdc++的deque,并且几乎完全符合C++标准。可以临时替换STL的,或者用来学习。

仓库在deque,性能测试在deque-benchmark,测试在deque-test

互动环节

最近有点忙,更新少尽量,尽量把内容堆多一些


上一期

本期

下一期

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