最近阳光明媚,天气晴朗,我为了更好地折腾理解 CMake,尝试把以前在 macOS 上写的一个 OpenGL 程序从 Makefile 改成 CMake 构建,顺带看看同样的东西在 Windows 上编译、运行效果如何。然而在 Windows 上用 MSVC 2015 构建后运行,发现场景里的东西都没有了,并且没有任何错误输出。
因为似乎坊间传言 Windows 对 OpenGL 的支持很迷,所以我的第一直觉是 GLSL 的兼容问题,不过这个角度似乎很难排查。
所以还是搞个最小的可重现问题的代码吧,于是从经典的「三角形」Hello World 开始很快地试了一下,发现在 Shader 里做矩阵乘法就有问题;进而发现只要不乘投影矩阵就没有大问题,至少场景里的东西还是可以显示出来的。
蛤?这是什么鬼?总不见得 glm::perspective
有移植性问题吧?
经过一番调试,终于找到了问题所在,结论让人哭笑不得:
我在代码里自己封装了摄像机类,初始化时需要设置投影矩阵所需的远近裁剪平面,相当于下面这样。
Camera(float far, float near)
: far_{far}, near_{near}
{
/* blah blah */
}
虽然在其它平台没什么问题,但 windows.h
里有这种东西:
#define far
#define near
于是代码就相当于变成了
Camera(float far, float near)
: far_{}, near_{}
{
/* blah blah */
}
把远近裁剪平面的两个变量都初始化成了 0,也难怪场景里什么东西都看不见了。
唉,写代码的时候忘记了 far / near 指针这种历史遗留爆点,不过还是要吐槽下 windows.h
如此暴力的解决方法。
以上,真是有意义的一天呀~
最近发现如果使用最新的 2.7.4 版本的 psycopg2,import psycopg2
时会出现警告:
UserWarning: The psycopg2 wheel package will be renamed from release 2.8; in order to keep installing from binary please use "pip install psycopg2-binary" instead. For details see: http://initd.org/psycopg/docs/install.html#binary-install-from-pypi
大致就是因为老版本中自带的预编译 wheel 二进制可能会造成崩溃问题,官方决定 psycopg2 从 2.8 版本开始将不再包含预编译二进制,必须通过编译安装。为此还作出了两个调整:
- 2.7.4 ~ 2.8 之间为过渡期,使用 psycopg2 的预编译二进制时会生成警告
- 新增了 psycopg2-binary 包,除了仍旧提供预编译二进制外,与 psycopg2 完全一致(即与老版本行为一致)。
找了一下相关的讨论,感觉比较赞同这个 Issue 中的反对观点:
- 推荐 psycopg2-binary 实际上是个换汤不换药的解决方案,什么问题都没有解决;
- 虽然 psycopg2-binary 的包名没变(还是
import psycopg2
),但一些第三方库可能因此产生依赖问题;
- 由于目前 Python 的包管理机制,psycopg2 和 psycopg2-binary 虽然可以共存,但删的时候会同时删掉,很滑稽。
官方在说明中推荐开发用 psycopg2-binary、生产用 psycopg2,我总觉得有些坏味道。(同时也懒 :-P)
如果对此无所谓,可以在导入时自行捕获警告。(如果是通过 SQLAlchemy 间接导入,则在 create_engine
时捕获即可。)
import warnings
with warnings.catch_warnings(record=True):
import psycopg2
个人觉得比较 Sane 的方法可能是在 pip install
时使用 --no-binary <NAME>
来跳过预编译二进制的使用。如果你和我一样用的是 Pipenv,则需要用环境变量来控制 pip 的行为:
$ PIP_NO_BINARY=psycopg2 pipenv install psycopg2
但愿官方能把 psycopg2-binary 取消掉。
以上,默默水了一篇。
在知乎上看到《constexpr 究竟有什么用?》的问题,正巧前段时间趁着了解 constexpr if 的时候刚做过相关资料的搜集,拼凑了一下搞了一个回答。篇幅所限,完整版放在这里,主要是追加一些例子并补完一些周边内容 😄
constexpr
关键词是 C++11 引入的;C++14 中放宽了对 constexpr 函数的语法要求;而 C++17 则复用了该关键字,引入了 constexpr if。
constexpr
主要为 C++03 引入了以下变动:
- 拓展了「常量表达式」的概念
- 提供了「强制要求」表达式在编译时求值(compile-time evaluation)的方法
- 提供了编译时的
if
条件判断
拓展常量表达式的概念
之所以要拓展常量表达式的概念,是因为 C++03 中的一些尴尬,这从原本的标准库中就能看出来。
比如我们都知道 INT_MAX
是 C 语言的遗产,C++ 则更希望大家使用 std::numeric_limits<int>::max()
来拿 int
型的上限。然而不幸的是,后者是个函数调用而不是整型常量,使用起来可能需要花更多心思在性能或者别的东西上,没有前者那么自由。
int foo[std::numeric_limits<char>::max()]; // error: 不是常量表达式
又比如标准文件流,它的构造函数可以带上这样的第二个参数:
std::fstream foo("foo.txt", std::ios::in | std::ios::out);
这个参数是 openmode
类型的,是由实现具体定义的一种 Bitmask 类型。出于类型安全的考虑,通常用枚举值实现而不是整型。但是这样一来就有个问题,同样是写 std::ios::in | std::ios::out
,如果用整型的话可以作为常量表达式使用,而为了类型安全考虑换用枚举实现(尤其是重载 operator|
)后,就再也不可能是常量表达式了。
inline openmode operator|(openmode lhs, openmode rhs)
{
return openmode(int_type(lhs) | int_type(rhs));
}
明明是这样简单的函数,对它的调用却不是常量表达式,就更别提编译时求值了。这就让委员会陷入了必须在「类型安全」和「效率」里二选一的尴尬境地。
标准库里会遇到这样的问题,大家日常使用也会遇到。加之标准委员会很想借此机会把原本标准中对于「常量表达式」(尤其是整型常量表达式)复杂的定义重构简化,引入 constexpr
来标记一些简单的函数,让对它们的调用能够作为常量表达式存在就很合情合理了。
而 constexpr 成员函数也可以类推,比独立的 constexpr 函数多一些对于类本身的限制便是了。
至于 constexpr 函数「到底应该多简单」,其实并没有必要刻意去记那些限制。如果你标记了 constexpr
的函数不够简单,编译器会提醒你哪里有问题的。
强制要求表达式编译时求值
说到这里,「constexpr 函数」并不能和「编译时求值」划等号,它只表达了「这个函数具备这样的能力」。
如果需要强制要求表达式在编译时求值,那么我们需要在变量定义前添加 constexpr
,这样,用来初始化这个变量的表达式就「必须」是常量表达式,否则会报错。
#include <fstream>
// 该函数不是 constexpr 函数
int identity(std::ios::openmode mode)
{
return mode;
}
constexpr auto out = std::ios::out; // 此处为 constexpr
int main()
{
constexpr int modeFail = std::ios::in | identity(out); // 出错
int modePass = std::ios::in | identity(out); // 正常
}
constexpr 函数只有同时满足
- 所有参数都是常量表达式
- 返回的结果被用于常量表达式(比如用于初始化 constexpr 数据)
才会触发编译时求值。如果只有参数是常量表达式而结果不是,那么是否触发编译时求值取决于具体实现。
constexpr if
C++17 的 constexpr if 严格意义上不是 constexpr
而是 if
的一部分。
constexpr if 的主要用途是简化模板代码(这也意味着除非你是「库作者」或者「模版狂魔」,很少会用到)。很多原本需要绕弯借助类型 Tag 或者 SFINAE 来实现,需要拆成 N 个函数的功能,可以借助 constexpr if 写到一个函数里。
以下面的 get_value
函数为例。这个函数正常情况下会把参数原样返回,但如果传入指针,则返回被指针指向的内容。
如果用简单的类型 Tag 来实现,需要拆成三个模板:
template <typename T>
auto get_value(T t, std::true_type) {
return *t;
}
template <typename T>
auto get_value(T t, std::false_type) {
return t;
}
template <typename T>
auto get_value(T t) {
return get_value(t, std::is_pointer<T>{});
}
如果用 SFINAE 来实现,需要拆成两个模板:
template <typename T, std::enable_if_t<std::is_pointer_v<T>, int> = 0>
auto get_value(T t) {
return *t;
}
template <typename T, typename std::enable_if_t<!std::is_pointer_v<T>, int> = 0>
auto get_value(T t) {
return t;
}
而如果用 constexpr if 来实现,可以简化为一个:
template <typename T>
auto get_value(T t)
{
if constexpr (std::is_pointer_v<T>) {
return *t;
} else {
return t;
}
}
也有人把 constexpr if 拿来代替当作 #if
用,相信你可以想象得出具体的用法。但……我觉得那是邪道,所以不写了 😛
constexpr
和 const
最初接触到 C++11 时,constexpr
和 const
的关系让我很是害怕,因为排列组合似乎很多。不过这回终于下了决心,整理了下,没有初见时想象的那么复杂,总共似乎也有两个情况。
在修饰数据(变量)时,constexpr
是隐含 const
语义的。同时,constexpr
只和变量本身有关,遇到类似 const int *p
和 int *const p
的破事时,建议还是写明:
constexpr int const foo = 42;
constexpr int foo = 42; // 和上面等价
constexpr int const *pb = &bar; // 此处 &bar 必须是常量表达式
在涉及 constexpr 成员函数时,成员函数可能会有 const
修饰的情况。C++11 中,constexpr 成员函数是隐含 const
的;C++14 以来则没有这个限制。这里似乎可以借用 Python 的哲学「Better explicit than implicit」来表达一下,总共就几个字母的事情,就别偷懒了,你没法保证别人和你一样清楚这些「不那么直观」的东西。
以上就是所有内容了,感觉还是有些流水账的样子。
顺便小小吐槽一下「模板」,macOS 自带的拼音输入法只能通过 mú bǎn 打出来,所以我老是打成「模版」……
「移动支付」是支付宝推出的针对手机移动支付的服务。虽然支付宝现在建议新商户转用「App 支付」接口了,但是一些较早接入的 App 仍旧在使用这个服务,比如鄙厂的智能证件照 ;-)
各大支付服务的套路其实都是一样的,无论网页还是 App:
- 服务器为支付所需数据签名
- 客户端使用签名后的数据调起支付服务
- 用户支付成功后,客户端获得支付结果同步通知,服务器获得异步通知回调
显然支付状态应以服务器获得的异步通知为准,不过某些情况下,客户端可能会有在先检查一下本地的支付结果有效性的需求。
前些天在 Sentry 里收到了错误报告,原因是校验客户端获得的同步通知结果时,发生了「被校内容格式异常」的错误。初以为是有人故意修改了客户端在捣乱,检查请求内容才发现,原来是支付宝(不知为何)本次发来的同步通知内容使用了不同以往的格式:
partner="2088101568358171"&seller_id="xxx@alipay.com"&out_trade_no="0819145412-6177"&subject="测试"&body="测试测试"&total_fee="0.01"¬ify_url="http://notify.msp.hk/notify.htm"&service="mobile.securitypay.pay"&payment_type="1"&_input_charset="utf-8"&it_b_pay="30m"&success="true"&sign_type="RSA"&sign="hkFZr+zE9499nuqDNLZEF7W75RFFPsly876QuRSeN8WMaUgcdR00IKy5ZyBJ4eldhoJ/2zghqrD4E2G2mNjs3aE+HCLiBXrPDNdLKCZgSOIqmv46TfPTEqopYfhs+o5fZzXxt34fwdrzN4mX6S13cr3UwmEV4L3Ffir/02RBVtU=";extendInfo="doNotExit":true,"isDisplayResult":true
末尾前无古人后无来者地加了个分号,然后以奇怪的格式引入了 extendInfo
和 isDisplayResult
两个字段。这是文档中从来没有提过会发生的事情。
原本通过文档中字段说明及例子,我们可能还觉得返回的支付结果就是用的 HTTP Query String 格式,而且支付结果去除涉及签名的 sign
及 sign_type
相关键值对后可以直接用来验证签名。收到这回这么一条回调,就都不成立了。
所幸原先为了偷懒没有以 HTTP Query String 的格式解析支付结果,而是直接用正则表达式 ^(.+)&sign_type="RSA"&sign="(.+)"$
从中提取待签名字符串及签名本身。(偷懒之处在于:因为如果解析以后,按照支付宝的签名规则,我还得再把它们重新排序;而解析前的键值对本身已经是排过序的了。)
但不幸的是,这个正则表达式认为 sign="xxxx"
后不应该有任何多余数据。于是便有了「被校内容格式异常」。
反思及吐槽
前两天刚在知乎大言不惭地在回答里说很多程序员不肯看文档、凭直觉掩耳盗铃地写代码,结果这就立马变成自我吐槽了 ;-(
其实仔细看支付宝的文档的话,关于同步通知中的支付结果和签名,是这样说的:
result:本次操作返回的结果数据。其中:&success="true"&sign_type="RSA"&sign="xxx"之前的部分为商户的原始数据。success用来标识本次支付结果。sign="xxx"为支付宝对本次支付结果的签名(加签内容为:案例中原始数据&支付结果,……)
首先,你看文档里确实并没有保证支付结果数据始终以 sign="xxx"
结尾。我在正则表达式里作出那样的假设,是基于例子推出的假设,但这其实是个高中数学「充要条件」的问题。
其次,我那偷懒利用结果数据中「已排序」特性的操作,虽然目前并没有什么问题,但其实也是有风险的。因为文档里并没有保证它的有序性。
所以嘛,我错了,我应该好好看文档的……
以上,真是有意义的一天呐~
自旋锁(spinlock)很好理解。对自旋锁加锁的操作,你可以认为是类似这样的:
while (抢锁(lock) == 没抢到) {
}
只要没有锁上,就不断重试。显然,如果别的线程长期持有该锁,那么你这个线程就一直在 while
while
while
地检查是否能够加锁,浪费 CPU 做无用功。
仔细想想,其实没有必要一直去尝试加锁,因为只要锁的持有状态没有改变,加锁操作就肯定是失败的。所以,抢锁失败后只要锁的持有状态一直没有改变,那就让出 CPU 给别的线程先执行好了。这就是互斥器(mutex)也就是题目里的互斥锁(不过个人觉得既然英语里本来就不带 lock,就不要称作锁了吧)。对互斥器加锁的操作你可以认为是类似这样的:
while (抢锁(lock) == 没抢到) {
本线程先去睡了请在这把锁的状态发生改变时再唤醒(lock);
}
操作系统负责线程调度,为了实现「锁的状态发生改变时再唤醒」就需要把锁也交给操作系统管理。所以互斥器的加锁操作通常都需要涉及到上下文切换,操作花销也就会比自旋锁要大。
以上两者的作用是加锁互斥,保证能够排它地访问被锁保护的资源。
不过并不是所有场景下我们都希望能够独占某个资源,很快你可能就会不得不写出这样的代码:
// 这是「生产者消费者问题」中的消费者的部分逻辑
// 等待队列非空,再从队列中取走元素进行处理
加锁(lock); // lock 保护对 queue 的操作
while (queue.isEmpty()) { // 队列为空时等待
解锁(lock);
// 这里让出锁,让生产者有机会往 queue 里安放数据
加锁(lock);
}
data = queue.pop(); // 至此肯定非空,所以能对资源进行操作
解锁(lock);
消费(data); // 在临界区外做其它处理
你看那个 while
,这不就是自己又搞了一个自旋锁么?区别在于这次你不是在 while
一个抽象资源是否可用,而是在 while
某个被锁保护的具体的条件是否达成。
有了前面自旋锁、互斥器的经验就不难想到:「只要条件没有发生改变,while
里就没有必要再去加锁、判断、条件不成立、解锁,完全可以让出 CPU 给别的线程」。不过由于「条件是否达成」属于业务逻辑,操作系统没法管理,需要让能够作出这一改变的代码来手动「通知」,比如上面的例子里就需要在生产者往 queue
里 push_back
后「通知」!queue.isEmpty()
成立。
也就是说,我们希望把上面例子中的 while
循环变成这样:
while (queue.isEmpty()) {
解锁后等待通知唤醒再加锁(用来收发通知的东西, lock);
}
生产者只需在往 queue
中 push_back
数据后这样,就可以完成协作:
触发通知(用来收发通知的东西);
// 一般有两种方式:
// 通知所有在等待的(notifyAll / broadcast)
// 通知一个在等待的(notifyOne / signal)
这就是条件变量(condition variable),也就是问题里的条件锁。它解决的问题不是「互斥」,而是「等待」。
至于读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。读写锁不需要特殊支持就可以直接用之前提到的几个东西实现,比如可以直接用两个 spinlock 或者两个 mutex 实现:
void 以读者身份加锁(rwlock) {
加锁(rwlock.保护当前读者数量的锁);
rwlock.当前读者数量 += 1;
if (rwlock.当前读者数量 == 1) {
加锁(rwlock.保护写操作的锁);
}
解锁(rwlock.保护当前读者数量的锁);
}
void 以读者身份解锁(rwlock) {
加锁(rwlock.保护当前读者数量的锁);
rwlock.当前读者数量 -= 1;
if (rwlock.当前读者数量 == 0) {
解锁(rwlock.保护写操作的锁);
}
解锁(rwlock.保护当前读者数量的锁);
}
void 以写者身份加锁(rwlock) {
加锁(rwlock.保护写操作的锁);
}
void 以写者身份解锁(rwlock) {
解锁(rwlock.保护写操作的锁);
}
如果整个场景中只有一个读者、一个写者,那么其实可以等价于直接使用互斥器。不过由于读写锁内部是至少需要用一把锁来保护当前读者数的,所以,如果你的临界区很小,读写锁相比一般的锁并不能带来很大的优势,甚至可能性能更低。
另一方面,读写锁要真正发挥效能,条件也比较麻烦。比如实际的读写锁通常不用例子里两把锁的实现,而是用一把锁、一个条件变量来实现,好处是可以缓解写者饥饿的情况(一旦有写者在等锁,后续读者都需要等写者离开后才能继续),但这样一来,如果读者的临界区没有明显小于写者的临界区,阻塞情况可能会变得比较不理想……
所以你可以认为读写锁是针对某种特定情景的「优化」。不是说不要用读写锁,而是读写锁往往没有看上去那么理想。个人建议是可以优先用 mutex,如果遇到瓶颈后可以选择替换为读写锁,看看能否带来性能提升。
以上。
- «
- 1
- 2
- 3
- 4
- 5
- ...
- 15
- »