0%

什么是向下转换

向下转换是可能出现溢出的转换。检查是否为向下转换:

template <typename To, typename From, typename = void>
struct NotNarrowing: std::false_type {};

template <typename To, typename From>
struct NotNarrowing<To, From, std::void_t<decltype(To{std::declval<From>()})>>: std::true_type {};

其中 To{std::declval<From>()} 这一步如果用在具体的类型上(比如说 int 和 unsigned),编译器是只给警告,不会报错。如果能够在编译期推导出来数值,则会根据数值来选择通过或报错。比如 char{'a'}; 是正常编译的,但是 char{300}; 是会报错的。

向下转换溢出的判断标准是真实的表数范围:

  1. 无符号数和对应的有符号数互转都算是溢出。
  2. 范围较小的无符号数转向范围较大的有符号数是不溢出的。
  3. 任何有符号数转向无符号数都会溢出,因为无符号数不能表示负数。

下面的代码继承了 std::streambuf 改变了 ostream 的行为,并自定义了打印操作将结果送到 stdout,而不是 std::cout 中(当然也可以更改逻辑修改打印的细节)。

其中的 print 函数是仿 C++23 std::print 的,功能仅仅是打印参数,在笔记 实现简单的花括号插值打印 中实现。

struct membuf : std::streambuf {
    membuf(char *p, size_t n) {
        setg(p, p, p + n);  // set read pointers
        setp(p, p + n);
    }

    void flush() {
        auto p = pbase(), e = pptr();
        fwrite(p, 1, e - p, stdout);
        setp(p, e);  // reset write pointers
    }

   private:
    int overflow(int c) override {
        flush();
        if (c != '\0') fputc(c, stdout);
        return c;
    }
};

// A little slower since we have to maintain a different buffer.
template <class... Ts>
void print_stdout(const char *s, Ts... args) {
    char buf[512];
    membuf mbuf(buf, sizeof(buf));
    std::ostream os(&mbuf);
    print(os, s, args...);
    mbuf.flush();
}

https://developer.nvidia.com/blog/cuda-pro-tip-understand-fat-binaries-jit-caching/

CUDA 程序有两种代码:一是设备无关的 PTX,二是设备有关的二进制代码。在运行之前将 PTX 编译成二进制代码就是 JIT 过程。(当然,nvcc 会在文件系统里面存储 cache。)用 -arch=sm_xx 可以只为给定的架构编译,从而运行时不需要 JIT 这一步。

禁用 cache(不允许本次运行读取和写入 cache):

export CUDA_CACHE_DISABLE=1

然后分别使用 -arch=native 选项和不使用,可以明显感受到程序在启动速度上的差别。

终止合并

可以用 git merge --abort,就不要用 git reset --hard 了。

合并的时候忽略空白字符

git merge 时加入参数 -Xignore-all-space or -Xignore-space-change

最好还是不用,因为这样容易出现混合的换行方式(\n\r\n)。

手动 re-merging

讲的是 git merge-file 的用法。和手动修改相比,这样做更容易脚本化,因为很多命令行工具只能对没有 conflict markers 的源码处理。

首先,冲突已经发生。我们获取冲突文件的三个版本:

https://git-scm.com/book/en/v2/Git-Tools-Rerere

rerere 的一些性质

rerere 没有默认开启。

rerere 是在前一步骤合并冲突,生成了含有冲突 markers 的文件之后,根据记录的结果重新应用冲突解决方案的。因而可以手动对冲突了的文件(通过 git checkout --conflict=merge -- <files> 创建)应用 git rerere

rerere 可以自动解决冲突,但是解决过程发生在冲突之后,也不会自动提交,因此 rerere 在实现上更像一个钩子。这样也可以给使用者再次检查的机会。解决冲突之后把 unmerged paths 加入到 index,然后用 git commit 或者 git rebase --continue 来确认。

rerere 的缺点

rerere 不知道什么合并方式是正确的、什么是错误的,因而会一律记录下来。比如有时候合并错误需要退回,这个时候重新合并,rerere 就会用错误的方式解决冲突。使用 git rerere forget <pathspec> 可以让 rerere 删除错误的合并方式,将 <pathspec> 换成 . 就是删除本文件夹的所有冲突解决方式(注意当前是否在 git 仓库的根目录)。见 https://stackoverflow.com/questions/5519244/are-there-any-downsides-to-enabling-git-rerere

格式解析

根据维基百科,deb 包是一个标准的 ar 归档。

因此,对于 Modern CMake for C++ 中示例代码用 CPack 创建的包 CPackPackage-1.2.3-Linux.deb,可以直接解压。

ar x CPackPackage-1.2.3-Linux.deb

解压可以得到三个东西:

.
├── control.tar.gz
├── data.tar.gz
└── debian-binary

tar 的时候可以指定压缩,要注意打包的时候如果用了压缩,解包的时候需要用同样的算法来解压。这个并不是自动识别的。比如:

tar -zcf xxx.tar.gz files 对应于 tar -zxf xxx.tar.gz

而且,如果不压缩就不要用 .tar.gz 来命名,而是用 .tar。(如果是其他压缩格式就用其他的命名方式。)

tar -C targetFolder ... 可以将文件解压到目标目录,但是要求目标目录首先存在。tar 命令不会帮助我们创建目录。

下载整个站点

# https://stackoverflow.com/a/11514515/
wget -r -p -np -k ${URL}

感觉 CMake 的语法不是很容易格式化,感觉怎么都别扭。现在我看到一种格式的思路比较好,就是把命令的每个选项都当成 yaml 配置文件去写,比如:

install(FILES
    src/include/calc/calc.h
    src/include/calc/nested/calc_extended.h
  DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/calc
)

这种列表就换行+缩进。

再次考虑 dot 计算

在第 5 章的 dot 计算中,我们在每个块上做完 reduction 之后就将数据拷贝回到 CPU 了,然后让 CPU 做最后的加法。

为什么在 compute capability 2.0 之前,atomicAdd 只支持整数?因为原子加法是不能指定计算的发生顺序的,因而每个计算都必须遵守结合律,也就是 $(A+B)+C$ 必须等于 $A+(B+C)$。但是浮点数因为中间结果的舍入问题,并不能保证这一点!!

本节接下来是讲解用原子操作实现一个忙等待的 mutex,用于同步多个 CUDA 线程(因为在写书的时候浮点数的原子加法还没有受到设备的广泛支持)。atomicCAS 就是 CUDA 上的比较并交换。

// mutex 的类型是 int *,而且是分配在 GPU 上的
__device__ void lock( void ) {
  while( atomicCAS( mutex, 0, 1 ) != 0 );
}

__device__ void unlock( void ) {
  atomicExch( mutex, 1 );
}

Note

这里的 unlock 方法并不是直接对 *mutex 赋值为 1,因为 CUDA 中原子操作和普通的内存访问经过的路径不同,所以应该在 unlock 的时候也统一使用原子操作。