懒惰是人类进步的源动力

  • iPad Pro 2020 简单评测 (对比 iPad Pro 10.5 2017)

    iPadPro_0.jpg

    生日的时候老婆大人送了一台iPad,相比之前使用的iPad Pro 2017 (10.5 inch)感觉提升还是挺大的。

    首先的提升是屏幕,大了0.5英寸, 屏占比提升了不少。其实2017 iPad Pro还有个奇怪的CPU过热问题:CPU发热量比较大,然后屏幕在CPU附近的区域会被“烤”黄。这个现象在白色显示的时候比较明显。不知道为什么,觉得屏幕素质还是比先辈提升了一点点。

    iPadPro_2.jpg

    CPU和内存方面有了显著提升,但是对我而言上手实际感觉出来不了多少。A10X变成了A12Z,内存从4GB提升到了6GB。但是库克还是比较省材料啊,想来安卓的手机内存都有8GB了。

    ipad_pro_4.png

    重量上面,这两台机器其实是很相似的。10.5寸款是477 g,新款轻了6g。如果不戴套的话,手持感觉相当棒~一般来说如果我看Kindle的话就会把套子去掉。推荐买那种背面磁吸的手机壳,拆装很方便!

    iPadPro_case.jpg

    本次更新最大的提升点是摄像头,但是鄙人用iPad摄像头的机会是少之又少。稍许试了一下,相比17款,摄像头的拍摄效果有巨大提升,特别是景深传感器感觉是下一代iPhone的提前布局。

    总而言之,如果手持是10.5寸Pad设备的话,强烈建议换机。但如果用的2018款的iPad Pro,除非要用到摄像头,否则不建议再花钱啦~

  • std::future 与 std::promise 简单使用、简单原理

    C++11有了一些关于线程的模型,在此之前C++里可是各自为政的,各种线程库各种神奇用法。其中有两个好玩的东西就是std::promise<T>std::future<T>,下文书中形容它们像是个“虫洞”。

    • std::future是虫洞的出口:一个未来对象的“获取器”,在未来的某一刻它能返回一个有用的值,但现在还没有…
    • std::promise是虫洞的入口:我们要保证在未来的某一刻给一个值进去,但不是现在…

    Basic usage

    一般来说这俩是成对使用的,看个代码:

    #include <cassert>
    #include <chrono>
    #include <future>
    #include <iostream>
    #include <string>
    #include <thread>
    
    using namespace std::chrono_literals;
    
    int main()
    {
      std::promise<int> p1, p2;
      std::future<int> f1 = p1.get_future();
      std::future<int> f2 = p2.get_future();
      
      p1.set_value(42);
      assert(f1.get() == 42);
      
      std::thread t([&]() {
        std::this_thread::sleep_for(100ms);
        p2.set_value(43);
      });
      
      auto start_time = std::chrono::system_clock::now();
      assert(f2.get() == 43);
      std::chrono::duration<double, std::milli> elapsed = std::chrono::system_clock::now() - start_time;
      std::cout << "Waited " << elapsed.count() << " ms\n";
      t.join();
      return 0;
    }
    
    // output: Waited 100.253 ms                                                                                                                                                       
    

    一个future对象可以通过promise.get_future()方法创建出来。当我们有真正的值来填进promose对象的时候,就用promise.set_value(v)方法。同时,(一般是在另一个线程里)当准备要获取值的时候,就调用future.get()方法。get方法会保持阻塞状态直到一个有效值被填进去。

    值得一提的是,有一个特化的T = void。这货有啥用呢?可以用来阻塞等待某个并行任务完成:

    std::promise<void> ready_p;
    std::future<void> read_f = ready_p.get_future();
    
    std::thread thread_b([&]() {
        prep_work();
        ready_p.set_value(); // no arg
        main_work();
    });
    ready_f.wait();
    // now that thread B has completed
    

    A bit of details

    需要留意的是,promise也好future也罢,都是有动态内存分配(dynamic memory allocation)的开销的。
    std_promise_future_schema.PNG(图源为参考文献)

    留意图中的那个State对象,它基本上是一个shared_ptr——因为虫洞的两端(很可能是不同线程)都要用到这个共享对象(shared ownership)。所以创建std::promose/std::future的时候都是要申请新的堆空间。

    Reference

    本文全文参考自本书:
    <Mastering the C++17 STL: Make Full Use of the Standard Library Components in C++17>

  • C++17 std::variant 简单使用

    std::variant应当是用来替代union来使用的,后者从C时代就有了,但是它缺乏类型安全的保障。boost库很早之前就有了实现(其实从C++11开始,感觉boost不知道多少东西被借鉴走了)。
    std::variant的类长这个样子:

    template <class... Types>
    class variant;
    

    Usage

    使用起来也是比较方便的

    std::variant<int, double> v1;
    v1 = 1; // activate the "int" member
    assert(v1.index() == 0);
    assert(std::get<0>(v1) == 1);
    
    v1 = 3.14; // activate the "double" member
    assert(v1.index() == 1);
    assert(std::get<1>(v1) == 3.14);
    assert(std::get<double>(v1) == 3.14);
    
    assert(std::holds_alternative<int>(v1) == false);
    assert(std::holds_alternative<double>(v1) == true);
    
    assert(std::get_if<int>(&v1) == nullptr);
    assert(*std::get_if<double>(&v1) == 3.14);
    

    上面代码里的几个好用的constexpr函数:

    • std::get<>:可以传入index或是type。如果index或者类型不对的话,会跑出std::bad_variant_access异常;
    • std::get_if<>:这个是不抛异常的版本。注意参数和返回值类型都是指针——如果没有有效元素,那就会返回nullptr;
    • std::holds_alternative<Type>:判断一个Type在不在定义的variant参数类里面;

    Visiting variants

    如果要根据当前存储的类型来对variant对象做一些操作,第一会想到写一些if语句

    if (std::holds_alternative<int>(v1) == true) {
      // handle int value
    }
    else {
      // handle double value
    }
    

    但是如果variant的可选类型比较多的话,定义一个Visitor类会有助于按类型来执行操作,逻辑会显得清晰

    struct Visitor {
      double operator() (double d) { return d; }
      double operator() (int i) { return double(i); }
    };
    
    void show(std::variant<double, int> v) {
      std::cout >> std::visit(Visitor{}, v) << std::endl;
    }
    
    void test() {
      show(3.14);
      show(1);
    }
    
  • C++17 std::generate的使用

    std::generate是“遍历——执行”工具函数的一种

    #include <algorithm>
    #include <iostream>
    #include <string>
    #include <vector>
    
    int main()
    {
      std::vector<std::string> v(4);
      std::generate(v.begin(), v.end(), [i=0]() mutable {
          return ++i % 2 ? "hello" : "world";
      });
      
      for (auto&& s : v) {
        std::cout << s << " ";
      }
      return 0;
    }
    

    上面的函数输出是hello world hello world .

    std::generate(beginIt, endIt, generator)函数接受起止的迭代器以及一个生成方法。

  • C++17 新特性—— if constexpr

    看代码说话

    template <typename Iterator>
    auto distance(Iterator begin, Iterator end) {
      using Traits = std::iterator_traits<Iterator>;
      if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename Traits::iterator_category>) {
        return end - begin;
      } else {
        auto result = typename Traits::difference_type();
        for (auto it = begin; it != end; ++it) {
          ++result;
        }
        return result;
      }
    }

    上面的代码用来判断两个iterator之间的距离。主要逻辑是判断iterator是否是可随机访问的iterator然后作分支处理,可以随机访问的迭代器直接把iterator相减。
    constexpr if语句中,条件的值必须是可按语境转换到 bool 类型的经转换常量表达式。若其值为 true,则舍弃 false分支语句(若存在),否则舍弃 true分支语句。有意思的一点是,被舍弃语句中的 return 语句不参与函数返回类型推导,也就是说两个分支语句中的return推倒出来的类型允许不一样。

    如果没有C++17这种怎么写呢?就是比较麻烦。
    一可以加一个参数,但是每个tag写一个

    template <typename Iterator>
    auto distance(Iterator begin, Iterator end, std::bidirectional_iterator_tag); // one for each tag type

    或者用SFINAE写两个

    template <typename Iterator>
    auto distance(Iterator begin, Iterator end, 
                  typename std::enable_if_t<std::is_base_of_v<一大坨>>* = nullptr);
  • std::unique_ptr 关于智能指针的种种

    这个域名也是突发奇想买到的,作为unique-ptr.com,为了对得起这个名儿,那就说一下智能指针算了。

    前传。裸指针和auto_ptr

    裸指针就是高度自治(要自己管)的指针,在符合基本法(误)的前提下,使用裸指针的人可以为所欲为。裸指针的一个问题是,它有点不符合现代C++内存管理的指导思想,使用者必须对它的生命周期负责,否则有可能发生:

    • 内存泄漏(memory leak):如果new一大片内存但是由于代码复杂到头来忘了释放,就有可能耗尽内存资源;内存泄漏还有一种情况:如果申请内存做事情做了一半抛出了异常——代码跑到了exception的花括号里之后,原来申请的内存直接没办法拿到并且释放;
    • 悬挂指针(dangling reference):指针已经被删除了,但其内存仍然在使用中;另外还有双重删除(double delete)的问题,当程序的某部分要删除一个已经被删除的指针时,即可出现这种情况;还有一种,指针所指的内存已经失效了(比如出了作用域了),但是后面的代码还用着指针;

    应该是为了缓解这类问题,C++ 98标准搞出来了个智能指针:auto_ptr。相当于加了一个“管家”,在对象的内部保存那个裸指针,在管家析构的时候自动释放裸指针的内容,如果赋值的话那就把裸指针的所有权转交给新的管家。
    但是由于移动(move)语义是C++11才有的,auto_ptr在实现的时候,如果在两个智能指针对象之间作赋值(operator=)或者拷贝构造,它的参数都是非const类型的。为啥呢?因为它是会调用original_auto_ptr.release()方法拿到原始裸指针。
    这个有点无奈的操作是为了实现指针所指对象的“占有权”管理:两个auto_ptr不允许管理同一个目标裸指针。在“拷贝”的时候,相当于做了“主权移交”。

    Copying an auto_ptr copies the pointer and transfers ownership to the
    destination: both copy construction and copy assignment of auto_ptr
    modify their right hand arguments, and the “copy” is not equal to the
    original.
    https://en.cppreference.com/w/cpp/memory/auto_ptr

    这种操作并不符合常人对“拷贝“的理解,更像是”剪切“操作。且正是因为这个问题,切不可把auto_ptr放到stl容器里面,否则会得到各种编译错误。典型案例是这个:https://stackoverflow.com/questions/111478/why-is-it-wrong-to-use-stdauto-ptr-with-standard-containers
    另外由于析构的时候是用delete,auto_ptr里也不能放指针数组(那种需要用delete[]).

    unique_ptr

    不同于auto_ptr,unique_ptr 的赋值运算符只接受典型地由 std::move 生成的右值。刚才说了,“右值”是C++11移动语义的产物,它比较好地解决了主权移交时的尴尬问题。这怎么说呢,一个智能指针的命运啊,当然要靠自我奋斗,但是也要考虑到历史的行程
    回顾看之前说的裸指针的两个问题:

    • 内存泄漏:由于析构函数里会delete管理的裸指针对象,所以在跑出对象作用域的时候,编译器会帮我们解决好问题,不用操心了;
    • 悬挂指针:缓解,但不能完全避免。

    一个例子是

    #include <iostream>
    #include <memory>
    #include <string>
        
    int main()
    {
      std::unique_ptr<std::string> str_ptr;
      {
          std::string tmp("bbbababa");
          str_ptr.reset(&tmp);
          std::cout << str_ptr.get() << std::endl;
          std::cout << *str_ptr << std::endl;
      }
      std::string tmp("xxxxxxx");
      std::cout << str_ptr.get() << std::endl;
      std::cout << *str_ptr << std::endl;
      return 0;
    }
    // Output:
    //   0x728562b4f060
    //   bbbababa
    //   0x728562b4f060
    //   xxxxxxx
    

    此外,数组的问题也通过特化std::unique_ptr<T[]> 得到了解决.
    可能有些同学会有性能方面的顾虑,觉得智能指针会比裸指针的操作性能差一点。坦白讲这个是有可能的,但是在实际使用的过程中,往往作profiling后会发现性能出现瓶颈的地方几乎不是智能指针。作为unique_ptr来讲,它不需要维护额外的状态,只是对象的创建和销毁要作一些特殊处理,平时使用的过程中完全可以当作裸指针来用。真正可能要担心性能问题的,是shared_ptr这一类允许共享所有权的智能指针。

  • 因子 收益率 因子暴露 因子载荷 什么东东?

    作为非金融科班的学生,发现有一些术语其实看上去不知所以然,但是理解了之后就会感叹一下:

    这tm不就是xxx么~

    记得刚开始搞量化的时候,经常碰到的词汇就是各种因子,以及因子暴露(factor exposure)因子载荷(factor loading)。然后我就去百度知乎Google搜啊,立马出来一大堆长篇大论告诉你它的由来,各种多因子模型。窃以为对于刚入门的我,最想知道的就是这个东西是什么意思,然后才是它背后的模型。

    那么问题来了

    因子暴露(aka. 因子载荷)是什么?因子暴露就是因子载荷。假设你的投资组合里有p只股票,并且你有m个因子,那么想象因子暴露是一个p * m的矩阵——每只股票在每个因子上的因子值。打个比方,如果你的因子是32个行业分类,那么这个矩阵的元素非0即1——1代表股票属于这个行业. 如果假定一只股票只能属于一个行业,那么这个矩阵的每一行都只可能有一个1,其余都是0.
    这个东西一般是归一化或者说标准化过的值,不然会无法做统计分析。

    那么,这东西能拿来做什么?

    结构化风险模型的假设是,投资组合的收益率是可以由下列公式解释

    r = X * b + u

    如果依旧假设有p只股票,m个因子,投资窗口时间长是l
    其中

    • r是投资组合收益,p*l矩阵
    • X是因子暴露,p*m矩阵
    • b是因子收益率,m*l矩阵
    • u是特异收益(又叫残差收益),相当于前面X*b不能解释的一部分,p*l矩阵
      简而言之就是一个投资组合的收益可以被几个因子来解释,解释不了的部分另归另说。

    如果假定个股残差收益是互不相关的,那么

    1. 比如我假定股票的收益(r)是由不同的行业因子决定的,那么只要有不同行业ETF的收益率(上面的b),我就可以拿来做回归分析得知股票对应的行业(上面的X);
    2. 再如假定股票的收益(r)和股票的Barra风格因子值(X)是已知的,那么也可以通过回归分析的方法拿到每个风格因子的收益率(b);

    题外话:感觉这个有点像期权定价里的implied volatility和realized volatility。如果是从underlying的价格用Black-Scholes公式算出来的叫隐含波动率,而已实现波动率可以通过观测期权价得到。上面通过ETF收益率算出来的因子暴露有点类似,其实股票的行业归属也可以从基本面的数据里拿到……

  • 记一次gtest EXPECT_DEATH遭遇sigsegv的debug过程

    the crash

    实习生小哥上周突然发现单元测试在dev机器上跑不起来了,–verbose一看发现是gtest报错Failure death test:
    gtest_expect_death_failure.png

    现象其实是所有写了EXPECT_DEATH的单元测试都挂掉了,感觉问题并不简单~ 问了一下边上的几位最近代码有什么变动,一说是用了个新的计算库。

    gdb

    gdb一波,除看代码发现这个测试的原理是起一个子进程,然后把stderr写到一个临时文件里,子进程退出后,从临时文件里读取log信息,和预期值比对。发现第一现象是stderr没抓到,结果在找stderr为什么没抓到的邪路上走了很久,发现根本没产生stderr:/tmp下面写的文件,大小是0字节。

    然后继续gdb,由于是要跟踪子进程,要把选项打开:

    set detach-on-fork off
    set follow-fork-mode child

    run了一下,看到了一点端倪:其实在fork之后进程就sigsegv了,而且居然是在动态加载glibc里的chdir函数过程中挂的。查了一下,机器上的glibc已经至少一年没有动过了,因此基本排除是glibc自己有问题。

    然后开始各种翻来覆去的看,先看了几个local变量,发现一切都显得那么完美,根本没有任何内存值被搞烂的迹象。接着只能看挂掉的现场了,info registers看到寄存器的值如下:
    gdb_info_registers.png

    突然发现红框处的rsp地址有点太“整”了,google了一下,确认这个地址应该是个用户态栈地址的下界。然后查看一下当前的指令,发现是一条push %ebx,也就是说还准备压栈————挂掉的清晰一些了,其实是栈地址空间不够了,也就是传说中的stack overflow。这也是为什么gtest能够看到程序挂掉,但是拿不到预期的错误信息的原因。

    the stack overflow

    那么,为什么栈空间不够了呢?翻了一下frame 0里的$rsp值,发现在0x7fc0附近,和刚才看到顶上的$rsp=0x7000相差了4032,也就是估计这个程序栈总的大小在4kb。查了一下ulimit -s有32767,诶? 于是接着看代码,发现gtest-death-test.cc里的做法, ExecDeathTestSpawnChild()里写道

    • mmap一个stack,它的大小是getpagesize() // ==4KB
    • clone一个新的线程来跑测试例,用的这个新创建出来的stack

    真相大白了。解决方法是mmap的时候分配多一点的栈空间。至于为什么到了上周才触发,大概是因为.so又多了几个吧~

    后记

    当然,其实这个bug早就有fix了,只是我是后来通过搜”ExecDeathTestChildMain stack overflow”这个关键词才找到的。之前搜了半天EXPECT_DEATH没什么结果~
    相关的PR有:

    所以说thirdparty code还是要偶尔更新一下的呀~

  • ASUS RT-AC1900P RT-AC68U 强行刷写Merlin方法

    最新的华硕固件(至少是AC68系列)已经开始阻止非官方固件的刷入,具体表现是能够上传固件,但是路由器重启之后其实什么都没发生变化,也并没有看到什么提示。
    对应这种情况,其实可以利用ASUS Firmware Restoration Tool强行刷写固件,也就是在恢复模式下更新固件。鄙人亲自试验表明,原有的系统配置在用这个工具强刷之后并不会改变(甚至Mesh节点都能继续连接)。

    连入救援模式并上传固件的方法,参考官方教程。主要步骤就是:

    1. 拔除路由器电源;把路由器LAN口用网线接入到电脑上,电脑上手工配置有线网卡的IP地址为192.168.1.10,子网掩码为255.255.255.0其余留空;
    2. 按下RESET键的同时接入路由器电源,保持RESET按下状态直到路由器的电源灯开始有规律闪烁;
    3. 打开下面的工具,直接上传Merlin固件;
    4. 上传成功后会自动重启的;

    固件更新程序下载(需要Windows系统)
    链接:https://pan.baidu.com/s/1zApE2OiOq7mX6bGgWGpNvA
    提取码:3idq

  • 联通8元全国流量王保号套餐变更指南

    起因是有了另外一张流量卡, 所以目前的冰激凌套餐也就不需要了,为了更换这个套餐,最终还是去了实体营业厅办理,好在一次成功,遂发个分享。众所周知联通早已取消了所有5元低保套餐,什么小天神之类的早已无法办理,因为她们不属于“在售套餐”了。目前联通最低消费的是这个4G全国流量王8元套餐,内容如下:

    全国流量王8元套餐详情:

    • 30分钟语音
    • 全国流量200MB

    套餐外

    • 全国流量:执行流量放心用,即按照0.1元/MB累计至10元,不再收费,直至1GB,按照10元/GB计费
    • 全国语音:0.15元/分钟 国内短、彩信:0.1元/条赠送来电显示;全国被叫免费

    10010客服——失败

    之前看这篇文章有先例,可以不走线下途径办理,所以打电话一试。
    打10010客服,人工服务后,对方明确表示有这个套餐,但是电话客服给的权限只能申请最低19元的套餐,再往下不行了。好说歹说回复我“帮我这边申请一下,不一定能成功”。然后过了3天,根本没有回访电话,于是我觉得走非投诉途径的话不去实体店是解决不了问题的。

    自营售后营业厅——成功

    务必前往自营的联通营业厅。当然跟业务员沟通需要技巧:业务员面露难色跟我说她能帮我把其他的阻止我变更套餐的东西退了,但是需要我自己打电话去10018或10010申请。
    鄙人打10018直接无反应,然后10010人工坐席全忙——这是为啥呢——现在想来可能是业务员把我的高增值业务退订光后,我就挪到在她们电话客服优先级队列的最底部了。然后我一边等人工坐席一边继续跟业务员argue,说之前打人工客服问过,对方明确表示一定要到营业厅办理,不信可以查电话客服记录。无语了1分钟后,业务员终于点击了鼠标完成了套餐变更,之前“没有权限”的说法也默默消失了~事实证明自有营业厅的客服人员是完全可以当场办理这个套餐变更的,只是她不想那么轻易把这个套餐变更的坏KPI挂在自己头上罢了。
    现在想来,如果当场发送CXXZ查询携转,并且以携号转网要挟,是不是态度会稍微好一点。

    建议

    结合以往经历,想要成功变更套餐,两条路:

    1. 工信部投诉:https://dxss.miit.gov.cn/
    2. 前往自有营业厅,不要怂,直接怼