C++ 中文周刊 第33期

reddit/hackernews/lobsters/摘抄一些c++动态

这周周末有事,发的比较早

周刊项目地址在线地址知乎专栏 腾讯云+社区

欢迎投稿,推荐或自荐文章/软件/资源等,请提交 issue


资讯

编译器信息最新动态推荐关注hellogcc公众号

OSDT Weekly 2021-10-13 第119期

QT 6出beta版本了

文章

大概内容,rust并没有比c++快和安全。唯一优点就是生命周期检查

很多代码场景下c++的灵活性要高于强制安全检查,且一些场景下rust生成的汇编不如c++少

serenity是一个c++写的操作系统,分享了一些开发记录/采访

一篇CRTP示例。主要解决的问题,基本接口实现,不需要virtual

TODO:看不懂讲的啥

用c写个lisp

讲各种各样的cast

这里着重介绍一下bit_cast,这个就是强制解释的memcpy版本,对于内建基础类型使用的,比如

#include <cstdint>
#include <bit>
#include <iostream>
 
constexpr double f64v = 19880124.0;
constexpr auto u64v = std::bit_cast<std::uint64_t>(f64v);
 
constexpr std::uint64_t u64v2 = 0x3fe9000000000000ull;
constexpr auto f64v2 = std::bit_cast<double>(u64v2);
 
int main()
{
    std::cout
        << std::fixed <<f64v << "f64.to_bits() == 0x"
        << std::hex << u64v << "u64\n";
 
    std::cout
        << "f64::from_bits(0x" << std::hex << u64v2 << "u64) == "
        << std::fixed << f64v2 << "f64\n";
}

实现就是memcpy硬拷,其实这种需求用union不就搞定了。多个copy换安全吗

考虑benchmark一段代码

bench_input = 42;
start_time = time();
bench_output = run_bench(bench_input);
result = time() - start_time;

这段代码的问题在于,编译器可能会重排time()导致run_bench的时间不准确

要保证,run_bench必须在两条time计算之间,不会被优化/重排 如何做?

google benchmark已经做过类似的工作DoNotOptimize()

bench_input = 42;
start_time = time();
DoNotOptimize(bench_input)
bench_output = run_bench(bench_input);
DoNotOptimize(bench_output)
result = time() - start_time;

DoNotOptimize()的作用是如何实现的?

inline BENCHMARK_ALWAYS_INLINE void DoNotOptimize(Tp& value) {
    asm volatile("" : "+r,m"(value) : : "memory");
}

这个asm可能看不懂,根据GNU extended inline asm syntax,是这个意思

asm asm-qualifiers ( AssemblerTemplate
                 : OutputOperands
                 [ : InputOperands
                 [ : Clobbers ] ])

针对这行asm,volatile暗示会变,让编译器不优化,AssemblerTemplate是空的,也就是明显是空的无作用的汇编也不要删掉?

“memory” 也就是 “clobbers memory” 明示直接内存读,也就是暗示这个值经常变

output constraints ("+r,m"(value) 明示读写这个value

是不是必须要用clobber memory?

有个类似的实现

inline BENCHMARK_ALWAYS_INLINE void EnsureMaterialise(Tp& value) {
    asm volatile("" : "+r,m"(value) : :); // Doesn't clobber memory.
}

也是暗示value会变,也是暗示value不被优化,但是不能保证value的全局副作用,还是会被重排,这个用来测试比如jit优化constant propagation优化之类的场景,看差异

我们要保证,value的计算是影响周围的调用的,所以,要标记value是可变只能从内存读/寄存器读读(clobber memory)这样就有全局副作用,对于相关的函数调用,能保证不被重排。

所以重新回顾一下上面这段代码

bench_input = 42;

// May have global side-effects.
start_time = time();

// Also may have global side-effects.
// Needs to observe any side-effects of `time()`, so can't be re-ordered before it.
// Forces `bench_input` to be materialized, despite it being a constant.
DoNotOptimize(bench_input)
// Here the compiler must assume that `bench_input` has now been mutated.

// Is expected to observe the potentially mutated value of `bench_input`, therefore
// cannot be reordered before `DoNotOptimize()`.
bench_output = run_bench(bench_input);

// May have global side-effects.
// Depends on `bench_output` so cannot be reordered above `run_bench()`.
DoNotOptimize(bench_output)

// Also may have global side-effects.
// Needs to observe any potential side effects of `DoNotOptimize(bench_output)`, so
// cannot be reordered before it.
result = time() - start_time;

视频

Core C++ 2021

有一些不是英语还没有字幕,实在看不懂,跳过

介绍了协程的几个猥琐用法

比如用于树的遍历,协程的栈比函数栈要省

Clang/gcc 用-Rpass可以看到优化的具体细节,不方便看?想要其他细节?借助工具,opt-viewer就是这么个工具,llvm组件里带的

但是有一定的缺点,CPU占用/内存之类的,作者改了一个optview2

并展示了一些用法示例,这个工具对于编译器分析有点帮助。

介绍了参数传递对性能的影响,列了一些极端场景。这里贴例子

传值比传引用重?传引用比传值轻?一般来说是,也有反例

STL中的场景

拷贝不可避免,比如accumulate,也更安全,比如transform

下面是几个好玩的例子(坑爹的用法)

const T&不一定是不可变的

void scale_down(vector<double>& v, const double& a) {
	for(auto&i : v) i /= a;
}
std::vector<double> a1{2, 2, 2};
scale_douw(a1,a1[0]);
// 1 2 2

我感觉这代码不喝两瓶啤酒写不出来

但是这种代码是有可能写出来的

#include <vector>
#include <iostream>
#include <iterator>
#include <string>
#include <sstream>
#include <cstdio>
using namespace std;

void inline print_vector(const std::vector<int> & v)
{
    ostringstream oss;
    copy(v.begin(), v.end(), std::ostream_iterator<int>(oss, " "));
    printf("%s\n", oss.str().c_str());
}
int main()
{
    vector<int> vec {1,2,3,4,5,6,1,2,3,4,5,6,1,2,3,4,5,6};
    vec.erase(std::remove(
        begin(vec), end(vec), 
                      *std::max_element(begin(vec), end(vec))),
          end(vec));
    print_vector(vec);
  // 1 2 3 4 5 1 2 3 4 5 6 2 3 4 5 6 
}

引用的位置对应的值变了,第一组1 2 3 4 5 6删掉了6,第二组的1补位,逻辑变成remove 1,然后2 3 4 5 6没删除,然后第三组 1 2 3 4 5 6,找到了1,最终就是这个效果

只能说,写代码的的时候少喝点酒

如何解决这个问题?把指针转换成值,强转一下,去掉指针信息,或者用decay_copy,原理都是一样的

template <class T>
  typename std::decay<T>::type
    decay_copy (T&& t) {
    return std::forward<T>(t);
  }

传引用反而比传值慢 godbolt

计算,传引用,寄存器利用效率不高,性能差, 用不上向量化

void byRef(std::vector<double>& v, const double& coeff) {
  for (auto& i : v) i *= std::sinh(coeff);
}

void byVal(std::vector<double>& v, double coeff) {
  for (auto& i : v) i *= std::sinh(coeff);
}

其实这背后有个问题,就是指针暗示着可能改动,所以不能尽可能 的优化,所以c中有restrict关键字,告诉你,这个指针在这个范围内不会被改,让编译器大胆做优化

作者还介绍了herb的一些实践,使用concept约束参数,以及思考std::ref stdx::val的用法等等。不过上面的代码例子是比较有意思值的看的了

python实现RAII

class Greeter:
  def __init__(self, name):
    self.name = name
    print(f"hello, {self.name}!")
    
  def __enter__(self):
    return self

  def __exit__(self, e_type, e_val, e_tb):
    print(f"goodbye, {self.name}!")

    
def main():
  with Greeter(1):
    print("we have a greeter")

main()

有了RAII,一个scopeguard就有了

class DtorScope:
  def __init__(self):
    self.stack = []

  def __enter__(self):
    return self
  
  def __exit__(self, e_type, e_val, e_tb):
    while self.stack:
      self.stack.pop().__exit__(e_type, e_val, e_tb)
      
  def push(self, cm):
    self.stack.append(cm)

然后可以结合闭包,装饰器模式

def cpp_function(f):
  def _wrapper(*args, **kwargs):
    with DtorScope():
      return f(*args, **kwargs)
    return _wrapper

这样就直接装饰main就行了

main = cpp_function(main)
main()

或者直接

@cpp_function
def main():
  ...

等一下,我们是c++周报,后面不展开了

PPT在这里,代码在这里

项目

看官方的例子

虽然是rust写的,但是是c++代码分析工具,所以放在这里了

TODO:有没有可能用c++重写?


本文永久链接

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