Skip to content

C++ 中文周刊 2024-11-23 第173期

Latest
Compare
Choose a tag to compare
@wanghenshui wanghenshui released this 23 Nov 14:48
1708e52

周刊项目地址

公众号

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

qq群 点击进入 满了加这俩 729240657 866408334

RSS

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

本期文章由 LH_mouse 赞助 在此表示感谢

祝老板身体健康事业顺利开新车装新房夜夜当新郎全世界都有你的丈母娘


资讯

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

来自本台记者Mick前方发来的报道

欢迎来到C++26的第五次会议,也是feature freeze之前的倒数第二次会议。本次会议共约230人参与,31个NB参会,依然是传统的线下:线上=2:1模式

在通过的提案方面,本次共通过8篇语言提案和19篇库提案。这比上次会议的7+12要多一些,和东京正好持平。

语言方面,最重磅的也是最坎坷的提案无疑是P1061 auto [...xs] = ... (structured binding packs),作为上次Plenary中被撤销的提案,P1061本来应该波澜不惊地再次投票。没想到的是,EWG第三天的讨论中MSVC开发者提出了对实现难度的抗议,差一点导致本提案倒在Stage 2。幸运的是,最后作者找到了一个妥协方案,将带pack的structured binding限制在只能在模版中使用,从而成功进入标准。除此之外,语言方面还通过了P3068,允许在编译期抛出异常。(当然,异常不能离开编译期,编译抛运行catch是不行的),并且常规deprecate了一批特性(is_trivial,不带逗号的varargs语法)。

LWG方面,最重磅的提案无疑是极度坎坷的P1928 std::simd。本提案的历史极其悠久,从2013年的N3759初创之后,-> N4184/5 -> N4395 -> P0214R9这14个revision之后终于在2018年修成半个正果,成功进入Parallelism TS v2。不过,随后的IS merge依然极度艰难,大规模的设计改动和名称反复贯穿了P1928的历史,最终在用了十年,30个revision之后终于成功进入C++26。本提案事实上大体标准化了SIMD指令,让标准中可以直接像操纵其他原生类型一样操作SIMD向量。至此,C++26标准库的两个T0和一个T0.5特性均已成功进入标准,L(E)WG成功提前完成了自己本周期的目标。下一次会议的主要目标看来就是搞定hive这个老大难。
另一个重点提案就是P3019 indirect/polymorphic,即一个deep copy版的智能指针。从cloned_ptr走到indirect_value走到indirect,花了P0201R6 -> P3019R11的19个revision才走完这条路,不过好歹是走完了。现在pimpl就可以用标准解法了。

除此之外,本次搞定了大量的线性代数bugfix提案,包括aligned_accessor,submdspan fix等对C++26至关重要的提案被成功完成(还剩下atomic_accessor和rank-2k两个提案,预计下次吧)。另外,本次会议还迈出了C23 rebase的第一步,成功将C23新增的两个头文件(bits,安全整数加法)加入了C++26。

本次会议的Stage 2工作组相对来说更有看点一些。EWG方面,P2786平凡迁移和P2900 Contracts均成功被推进Stage 3,但是两者的争议都非常大,forwarding poll可以说是barely consensus,要避免Plenary的失败还有很长的路要走。第三天的讨论中,反射终于确定了使用^^语法(unibrow),并逐渐开始在一些post-P2996反射提案上有了进展(比如consteval block,可惜expansion statement依然卡死着)。第四天早上是模式匹配的主场,虽然这一特性进入26的希望已经非常渺茫,但是P2688仍然在为此努力,并成为了EWG选择的语法而不是P2392的is/as。较为遗憾的是,原本应该在本次会议通过的fiber_context在最后一刻找到了Windows下的实现难题,只得推迟到下个周期去了。

LEWG方面,P2996反射和P2900 Contracts同样进入了Stage 3,从而扫清了这两个语言的主要目标在库这边的障碍。除此之外,整周大部分都在搞S&R相关扩展,例如async_scope,system scheduler等对P2300发挥作用至关重要的补充提案拿到了一定进展(所以lazy啥时候有人愿意接手…)。遗憾的是,concurrent queue依然在Concurrency TS v3和IS之间举棋不定,进入26的希望已经较为渺茫。除此之外,type_order_v也进入了Stage 3,有望给所有类型一个标准化的偏序关系。

Stage 1工作组方面,SG9 Ranges完成了Range化的并行算法的设计,但是离自己的plan依然差的有点远()。SG21 Contracts在完善Wording的同时,已经渐渐转向post-MVP特性,例如把语言UB大部分转成Contracts。SG23 Security完成了Profile的初版设计,不过前景究竟如何还要看看

展望明年的会议总体情况,“双边反转”已经基本成为事实,20/23周期的末尾都是LWG提案太多完不成不得不扔掉一些,这次LWG已经完成了绝大多数大提案,队伍反而不是很挤。提案太多完不成的变成了CWG,队伍里三个大提案实在有点吃不消,要做好CWG扔掉一堆小提案的准备。

编译器信息最新动态推荐关注hellogcc公众号 本周没更新 点击跳转上周的

文章

Exploring C++ std::span – Part 4: Const-Correctness and Type-Safety

还是介绍span的优缺点。这里介绍一种API冗余的问题

#include <span>
#include <iostream>
#include <vector>
void processData(int* data, std::size_t size) {
    std::cout << "fucked";
}
void processData(std::span<int> data) {
    std::cout << "ok";
}


int main() {
    std::vector<int> v{ 1,3, 5,7, 9};
    processData({v.data(), v.size()});
}

两种接口存在误用可能,保留一个即可

How do I put a non-copyable, non-movable, non-constructible object into a std::optional?

The operations for reading and writing single elements for C++ standard library maps

raymood chen这俩文章是连着的,我就放在一起讲了

一个是如何处理optional构造不能复制/移动的对象,简单来说解决方案就是类似std::elide

看代码 godbolt

#include <vector>
#include <string>
#include <iostream>
#include <optional>

struct Region {
        int dummy[100];
};
struct Widget
{
    Widget(Region const& region) {

    }
    Widget() = delete;
    Widget(Widget const&) = delete;
    Widget(Widget &&) = delete;
    Widget& operator=(Widget const&) = delete;
    Widget& operator=(Widget &&) = delete;

    static Widget CreateInside(Region const& region) {
        std::cout << "inside\n";
        return Widget(region);
    }
    static Widget CreateOutside(Region const& region) {
        std::cout << "outside\n";
        return Widget(region);
    }
private:
    int dummy[100];
};


struct WidgetInsideRegionCreator
{
    WidgetInsideRegionCreator(Region const& region) : m_region(region) {}
    operator Widget() { return Widget::CreateInside(m_region); }
    Region const& m_region;
};

template<typename F>
struct EmplaceHelper
{
    EmplaceHelper(F&& f) : m_f(f) {}
    operator auto() { return m_f(); }
    F& m_f;
};


int main()
{
    Region region;
    // construct with a Widget value
    std::optional<Widget> o0(WidgetInsideRegionCreator(region)); //函数声明我草
    std::optional<Widget> o{WidgetInsideRegionCreator(region)};

    std::optional<Widget> o2;
    // or place a Widget into the optional
    o2.emplace(WidgetInsideRegionCreator(region));
    // construct with a Widget value
    // 为什么这个不被解析成函数声明?
    std::optional<Widget> o3(
        EmplaceHelper([&] {
            return Widget::CreateInside(region);
        }));
    std::optional<Widget> o4;
    // or place a Widget into the optional
    o4.emplace(EmplaceHelper([&] {
            return Widget::CreateInside(region);
        }));
}

简单来说是通过optional的成员构造,从T本身构造,通过wrapper转发给optional隐式构造

第二篇文章是 map插入接口复杂以及带来的构造问题

Operation 操作 Method 方法
Read, throw if missing读取,如果缺失则抛出异常 m.at(key)
Read, allow missing 阅读,允许缺失 m.find(key)
Read, create if missing阅读,如果不存在则创建 m[key]
Write, nop if exists, discard value写入,如果存在则 nop,丢弃值 m.insert({ key, value })
m.emplace(key, value)
Write, nop if exists写入,如果存在则 nop m.emplace(std::piecewise_construct, ...)
m.try_emplace(key, params)
Write, overwrite if exists编写,如果存在则覆盖 m.insert_or_assign(key, value)

这么多接口,想实现如果没有就插入,如果有不要浪费构造这种逻辑,怎么做?

template<typename Map, typename Key, typename... Maker>
auto& ensure(Map&& map, Key&& key, Maker&&... maker)
{
    auto lower = map.lower_bound(key);
    if (lower == map.end() || map.key_comp()(lower->first, key)) {
        lower = map.emplace_hint(lower, std::forward<Key>(key),
            std::invoke(std::forward<Maker>(maker)...));
    }
    return lower->second;
}

auto& ensure_named_widget(std::string const& name)
{
    return ensure(widgets, name,
        [](auto&& name) { return std::make_shared<Widget>(name); },
        name);
}

一坨屎啊,注意到我们转发的这个逻辑,其实就是上面的elide

之前在115期也介绍过 优化insert

insert可能不成功,所以需要推迟value构造,惰性构造

template <class F>
struct lazy_call {
    F f;

    template <class T> operator T() { return f(); }
};

#define LAZY(expr) lazy_call{[&]{ return expr; }}
auto [iter, success] = map.try_emplace(key, LAZY(acquire_value()));

其实就是elide

应该有机会合入,代码也很简单 godbolt

那我们可以用emplacehelper,也就是elide重新实现一下,就用try_emplace就好了

template<typename Map, typename Key, typename... Maker>
auto& ensure(Map&& map, Key&& key, Maker&&... maker)
{
    return *map.try_emplace(key, EmplaceHelper([&] {
        return std::invoke(std::forward<Maker>(maker)...);
    }).first;
}

一行,甚至都不用封装这么丑的代码,直接在用到map的地方直接调用就行,比如

auto& item =
    *widgets.try_emplace(name, EmplaceHelper([&] {
        return std::make_shared<Widget>(name); })).first;

Some Of My Experience About Linking C/C++ On Linux

简单来说就是符号查找的问题

比如不同库可能的符号覆盖,符号weak strong之类的说明,不了解的建议翻翻《链接装载库》,虽然旧也够用

引申阅读 问题排查:C++ exception with description “getrandom“ thrown in the test body

找不到符号 -> 宿主机没提供 编译问题,版本没对上,实现链接了不存在的符号

Retrofitting spatial safety to hundreds of millions of lines of C++

加固详细介绍

同事kpi汇报

google 安全问题实践,使用libc++ harden mode,性能损耗不大,但是安全问题显著降低,完成了今年的KPI

他们测试数据性能影响不到1%,非常可观

加固只有1% 3%性能损失?

While these new runtime safety checks improve security, they add additional runtime overhead and can negatively impact performance. We studied the performance degradation for Google workloads and Feedback Direct Optimization (FDO) proved to be effective in minimizing it. As an example, enabling the hardened libc++, without any FDO, in a representative Google fleet workload added a ~0.9% queries per second (QPS) regression and a ~2.5% latency regression. When properly using FDO, we measured a ~65% reduction in QPS overhead and a ~75% reduction in latency overhead.

存在问题

  • 加固覆盖的还是有遗漏,目前还在修复
  • 有的加固存在ABI影响,不方便推广

但这个比起使用debug模式或者FORTIFY_SOURCE 之类的安全模式的开销要低的多

现在contracts没有实现之前大方向是向harden mode切换

std::any实现问题

最近看到个不用typeinfo的map实现,使用类型擦除加模拟,type使用static 变量地址或者类似的typemap技术实现

类型擦除带来的new delete开销不小

但去掉typeinfo/sso通过类型擦除带来的get收益也不小

看godbolt结果很奇怪

需要调查

  • 构造慢的原因?
  • get快的原因?

How we made Blurhash 128x faster

blurhash encode计算存在以下问题

  • 没有缓存冗余计算结果
  • 潜在的SIMD优化空间

作者根据这俩思路优化了128倍

不过我观察这项目虽然星不少但是大多只管调用没啥性能优化开发的,这个PR作者有心了,不过已经挂哪里快俩月了

pr在这里

ChibiHash: Small, Fast 64 bit hash function

手把手带你了解ChibiHash的实现原理,他的测试说性能比xxhash好,需要测一下

代码直接贴了

他这个性能好,但是碰撞概率还是会很高的,他自己也说了,感觉不如wyhash

// small, fast 64 bit hash function.
//
// https://github.com/N-R-K/ChibiHash
// https://nrk.neocities.org/articles/chibihash
//
// This is free and unencumbered software released into the public domain.
// For more information, please refer to <https://unlicense.org/>
#pragma once
#include <stdint.h>
#include <stddef.h>

static inline uint64_t
chibihash64__load64le(const uint8_t *p)
{
	return (uint64_t)p[0] <<  0 | (uint64_t)p[1] <<  8 |
	       (uint64_t)p[2] << 16 | (uint64_t)p[3] << 24 |
	       (uint64_t)p[4] << 32 | (uint64_t)p[5] << 40 |
	       (uint64_t)p[6] << 48 | (uint64_t)p[7] << 56;
}

static inline uint64_t
chibihash64(const void *keyIn, ptrdiff_t len, uint64_t seed)
{
	const uint8_t *k = (const uint8_t *)keyIn;
	ptrdiff_t l = len;
    // 写成了十六进制形式的 e(欧拉数)、π(圆周率)和黄金比例,最后一位调整为奇数(因为乘以偶数是不可逆的)
	const uint64_t P1 = UINT64_C(0x2B7E151628AED2A5);
	const uint64_t P2 = UINT64_C(0x9E3793492EEDC3F7);
	const uint64_t P3 = UINT64_C(0x3243F6A8885A308D);

	uint64_t h[4] = { P1, P2, P3, seed };

	for (; l >= 32; l -= 32) {
		for (int i = 0; i < 4; ++i, k += 8) {
			uint64_t lane = chibihash64__load64le(k);
			h[i] ^= lane;
			h[i] *= P1;
			h[(i+1)&3] ^= ((lane << 40) | (lane >> 24));
		}
	}

	h[0] += ((uint64_t)len << 32) | ((uint64_t)len >> 32);
	if (l & 1) {
		h[0] ^= k[0];
		--l, ++k;
	}
	h[0] *= P2; h[0] ^= h[0] >> 31;

	for (int i = 1; l >= 8; l -= 8, k += 8, ++i) {
		h[i] ^= chibihash64__load64le(k);
		h[i] *= P2; h[i] ^= h[i] >> 31;
	}

	for (int i = 0; l > 0; l -= 2, k += 2, ++i) {
		h[i] ^= (k[0] | ((uint64_t)k[1] << 8));
		h[i] *= P3; h[i] ^= h[i] >> 31;
	}

	uint64_t x = seed;
	x ^= h[0] * ((h[2] >> 32)|1);
	x ^= h[1] * ((h[3] >> 32)|1);
	x ^= h[2] * ((h[0] >> 32)|1);
	x ^= h[3] * ((h[1] >> 32)|1);

	// moremur: https://mostlymangling.blogspot.com/2019/12/stronger-better-morer-moremur-better.html
	x ^= x >> 27; x *= UINT64_C(0x3C79AC492BA7B653);
	x ^= x >> 33; x *= UINT64_C(0x1C69B3F74AC4AE35);
	x ^= x >> 27;

	return x;
}

Memory Subsystem Optimizations – The Remaining Topics

这个介绍了一些优化技巧边角料,这个主题我准备全翻译一下,这里标记一个TODO,感兴趣的可以直接看

简单说就是

  • 查表不一定比向量化计算快
  • 矩阵拍扁有局部性收益以及更少的内存碎片
  • 矩阵转置加速计算
  • 顺序读取写入性能更好 (这个应该是常识)
  • 如何更好的过滤/跳过
    • 模拟链表 类似稀疏,修改代价低但数据冷
    • 复制移动 数据总是热点,适合小数据类型
    • 复制开销/访问开销的取舍 测试优先
  • 循环倒置!双重循环,小循环在内部,因为数据局部性
  • 树/图 部分排序带来更好的局部性,查找更快?

性能测试就不列举了

C++ programmer's guide to undefined behavior: part 7 of 11

代码鉴赏环节

  • string_view别当char *用
#include  <iostream>
#include <string_view>
void print_me(std::string_view s) {
    printf("%s\n", s.data());
}

int main() {
  char next[] = {'n','e','x','t'};
  char hello[] = {'H','e','l','l','o', ' ',
                  'W','o','r','l','d'};
  std::string_view sub(hello, 5);
  std::cout << sub << "\n";
  print_me(sub);
}

挂sanitize=address会报错。godbolt

另外这个代码不挂sanitize可能会炸,我godbolt没复现

  • make_shared 无法访问私有构造?

使用friend tag绕过,非常猥琐 godbolt

struct Arg1 {};
struct Arg2 {};

class MyComponent {
private:
    struct private_ctor_token {
        friend class MyComponent; // 就是这个绕过
        private:
            private_ctor_token() = default;
    };

public:
    static auto make(Arg1 arg1, Arg2 arg2) -> std::shared_ptr<MyComponent> {
        return std::make_shared<MyComponent>(private_ctor_token{}, std::move(arg1), std::move(arg2));
    }

    // Ban copy and move constructors to avoid accidentally copying and moving
    // the object data into a stack instance.
    MyComponent(const MyComponent&) = delete;
    MyComponent(MyComponent&&) = delete;
    // Ban those bros too, that's optional.
    MyComponent& operator = (const MyComponent&) = delete;
    MyComponent& operator = (MyComponent&&) = delete;


    MyComponent(private_ctor_token, Arg1, Arg2) {  };

};
  • 乱用 std::aligned_*

主要问题,aligned_*_t 和 aligned_* 容易误用,漏掉**_t** 比如 godbolt

#include <memory>
#include <xmmintrin.h>
#include <type_traits>


const size_t head = 0;
std::aligned_union<256, __m128i> storage;
//std::aligned_union_t<256, __m128i> storage;
int main() {
    __m128i* a = new (&storage) __m128i();
    __m128i* b = new ((char*)(&storage) + 0) __m128i();
    __m128i* c = new ((char*)&storage + head) __m128i();
}

挺抽象 见 P1413

解决方案就是用alignas替换

 // To replace std::aligned_storage...
 template <typename T>
 class MyContainer {
    // [...]
   private:
-    std::aligned_storage_t<sizeof(T), alignof(T)> t_buff;
+    alignas(T) std::byte t_buff[sizeof(T)];
     // [...]
 };

 // To replace std::aligned_union...
 template <typename... Ts>
 class MyContainer {
    // [...]
   private:
-    std::aligned_union_t<0, Ts...> t_buff;
+    alignas(Ts...) std::byte t_buff[std::max({sizeof(Ts)...})];
     // [...]
 };
  • 隐式转换问题 比如string构造 0被识别成nullptr 用nullptr初始化string必炸,使用concept约束来限制
#include <cstdint>
#include <string_view>
#include <string>
#include <concepts>

struct MetricSample{
    // To avoid implicit conversions, we immediately
    // added 'explicit', as all the best practices recommend.
    explicit MetricSample(std::same_as<double> auto val): value {val} {}
private:
    double value;
};

class Metrics {
public:
    // To make it clear for the user why to allocate memory for string and avoid redundant implicit copies,
    // we use a rvalue reference.
    // After all, this is a great way to show that the interface intends to take ownership of the string.
    // A user should explicitly perform a move operation.
    void set(std::string_view metric_name, MetricSample val, std::string&& comment) {};
    // void set(std::string_view metric_name, MetricSample val, std::same_as<std::string> auto&& comment) {};
};


int main() {
    Metrics m;
    auto val = MetricSample(1.0);
    m.set("Metric", val, 0);
}

必炸 godbolt

  • range存在惰性求值行为,不要传引用,因为引用到计算的地方可能死了

有个talk得看一下 Keynote Meeting C++ 2022. Nico Josuttis. Belle Views on C++ Ranges, their Details and the Devil.

看一下这个例子 观察drop take语义区别 godbolt

#include <string>
#include <ranges>
#include <list>
#include <iostream>
#include <format>


void print_range(std::ranges::range auto&& r) {
    for (auto&& x: r) {
        std::cout << x << " ";
    }
    std::cout << "\n";
}

void test_drop_print() {
    std::list<int> ints = {1, 2, 3 ,4, 5};
    auto v = ints | std::views::drop(2);
    ints.push_front(-5);
    print_range(v);
}

void test_drop_print_before_after() {
    std::list<int> ints = {1, 2, 3 ,4, 5};
    auto v = ints | std::views::drop(2);
    print_range(v);
    ints.push_front(-5);
    print_range(v);
}

void test_take_print() {
    std::list<int> ints = {1, 2, 3 ,4, 5};
    auto v = ints | std::views::take(2);
    ints.push_front(-5);
    print_range(v);
}

void test_take_print_before_after() {
    std::list<int> ints = {1, 2, 3 ,4, 5};
    auto v = ints | std::views::take(2);
    print_range(v);
    ints.push_front(-5);
    print_range(v);
}


int main() {
    std::cout << "drop: \n";
    test_drop_print();
    std::cout << "------\n";
    test_drop_print_before_after();

    std::cout << "take: \n";
    test_take_print();
    std::cout << "------\n";
    test_take_print_before_after();
    
}

打印

drop: 
2 3 4 5 
------
3 4 5 
3 4 5 
take: 
-5 1 
------
1 2 
-5 1 

由于range的惰性,执行operator | drop并没有完成计算,只有range被访问才会真正计算,并计算一次drop

上面例子使用的是&& 同一个对象执行一次,如果改一下,改成传值,又会不一样

同一个view只会计算begin end一次,每次都是新的view,每次都会计算drop。

而take每次都会计算,不会缓存结果

代码就不贴了,godbolt

开源项目介绍

  • asteria 一个脚本语言,可嵌入,长期找人,希望胖友们帮帮忙,也可以加群753302367和作者对线

上一期 下一期