CPU 频率、超标量和IPC

X86处理器可以通过CPUID 指令获取CPU 的基频(Base frequency),但嵌入式CPU 往往没有提供这样的指令,另一种朴素而有效的思路是:用高精度时钟计量N条指令执行时间来计算CPU 当前频率。这种方法在Linux 启动时也会用到,毕竟不是所有CPU 都支持CPUID 或类似指令。

简单而直观的例子

下面是一段简单而直观的例子,通过累加寄存器1000次(CPU 执行一次累加需要一个时钟周期)计量CPU 频率。

#define INC(cnt) "inc %[cnt] \n"
#define INC10(cnt) INC(cnt) INC(cnt) INC(cnt) INC(cnt) \
INC(cnt) INC(cnt) INC(cnt) INC(cnt) INC(cnt) INC(cnt)
#define INC100(cnt) INC10(cnt) INC10(cnt) INC10(cnt) INC10(cnt) \
INC10(cnt) INC10(cnt) INC10(cnt) INC10(cnt) INC10(cnt) INC10(cnt)
#define INC1K(cnt) INC100(cnt) INC100(cnt) INC100(cnt) INC100(cnt) \
INC100(cnt) INC100(cnt) INC100(cnt) INC100(cnt) INC100(cnt) INC100(cnt)

/**
 * should compile the code with no optimization, gcc -O0
 */
void measure0()
{
    uint64_t start, end;
    int temp;

    // 高精度计时开始 tStart
    __ams( INC1K(temp) : [cnt] "+r"(temp));
    // 高精度计时结束 tEnd

    printf("CPU frequency : %d Hz\n", 1000/(tEnd-tStart));  //计算频率
}

上面的被测代码汇编后代码如下(eax 寄存器缓存了栈上的temp 数值,并自增了1000 次):

   0x000000000000202c <+47>:	mov    -0x24(%rbp),%eax
   0x000000000000202f <+50>:	inc    %eax
   0x0000000000002031 <+52>:	inc    %eax
   0x0000000000002033 <+54>:	inc    %eax
   ... ...
   0x00000000000xxxxx <+58>:	inc    %eax
   0x00000000000xxxxx <+250>:	mov    %eax,-0x24(%rbp)

注意:1 高精度时钟选择对结果的影响较大;2 增大测量的时钟周期越长,结果CPU 的频率更精确;3 不能添加编译优化选项。

该方法主要难度在于:精确编写运行期望CPU cycles的代码。

下面从反面来看看哪些示例会更多/更少地执行了期望CPU cycles:

错误1:访问非寄存器导致CPU stall

#define INC(dd) __asm("inc %[counter] \n" : [counter] "+r"(dd));
#define INC10(dd) INC(dd) INC(dd) INC(dd) INC(dd) INC(dd) \
INC(dd) INC(dd) INC(dd) INC(dd) INC(dd)
#define INC100(dd) INC10(dd) INC10(dd) INC10(dd) INC10(dd) \
INC10(dd) INC10(dd) INC10(dd) INC10(dd) INC10(dd) INC10(dd)
#define INC1K(dd) INC100(dd) INC100(dd) INC100(dd) INC100(dd) \
INC100(dd) INC100(dd) INC100(dd) INC100(dd) INC100(dd) INC100(dd)

void measure1()
{
    uint64_t start, end;
    int temp;

    // 高精度计时开始 tStart
    INC1K(temp);
    // 高精度计时结束 tEnd

    printf("CPU frequency : %d Hz\n", 1000/(tEnd-tStart));  //计算频率
}

原因分析:部分汇编的代码是这样的。

   0x000000000000202c <+47>:	mov    -0x24(%rbp),%eax
   0x000000000000202f <+50>:	inc    %eax
   0x0000000000002031 <+52>:	mov    %eax,-0x24(%rbp)
   0x0000000000002034 <+55>:	mov    -0x24(%rbp),%eax
   0x0000000000002037 <+58>:	inc    %eax

汇编后的代码没有被优化,CPU 每次都需要从Cache/Memory 中获取temp 数据并回写,导致不能在每个时钟周期都执行一条inc 指令。

错误2:被测代码指令级并行使得运行CPU cycles 小于期望

如果被测代码中自加的变量不止一个,如下

    __asm volatile(
    "cyclemeasure2:\n"
    "    dec %[counter] \n"
    "    dec %[counter] \n"
    "    dec %[counter] \n"
    "    dec %[counter] \n"
    "    dec %[counter2] \n"
    "    dec %[counter2] \n"
    "    dec %[counter2] \n"
    "    dec %[counter2] \n"
    "    jnz cyclemeasure2 \n"
    : /* read/write reg */ [counter] "+r"(cycles[0]), [counter2] "+r"(cycles[1])
    );  

而counter 和counter2 又没有数据依赖关系,那么那么在同一个CPU cycle 中同时被执行(超标量),花费的时钟周期略大于counter2 初始值,但远小于两倍counter2。从其汇编结果可以看出来,dec %eax 和dec %edx 可以在指令集并行。

   0x00000000000013c0 <+69>:	mov    -0x10(%rbp),%edx
   0x00000000000013c3 <+72>:	mov    -0xc(%rbp),%eax
   0x00000000000013c6 <+75>:	dec    %edx
   0x00000000000013c8 <+77>:	dec    %edx
   0x00000000000013ca <+79>:	dec    %edx
   0x00000000000013cc <+81>:	dec    %edx
   0x00000000000013ce <+83>:	dec    %eax
   0x00000000000013d0 <+85>:	dec    %eax
   0x00000000000013d2 <+87>:	dec    %eax
   0x00000000000013d4 <+89>:	dec    %eax
   0x00000000000013d6 <+91>:	jne    0x13c6 <measure2p+75>

循环减小代码段长度

生成那么太长的被测代码段可能导致iCache miss 或缺页中断,影响测试结果,上面示例中给除了基于循环的测试代码

int cycles = 65536;

rdtsc(start);
__asm volatile(
"cyclemeasure3:\n"
"    dec %[counter] \n"
"    dec %[counter] \n"
"    dec %[counter] \n"
"    dec %[counter] \n"
"    jnz cyclemeasure3 \n"
: /* read/write reg */ [counter] "+r"(cycles),
);  
rdtsc(end);

有一点需要注意:虽然每执行若干次dec 指令紧接着一次判断跳转指令jnz,但得益于现代CPU 的指令融合(称作instruction-fusion/micro-fusion,将比较指令及其之前的一个微指令合并为一个执行),jnz 并不会单独占用一个时钟周期,因此总的执行周期和cycles 初始值一致。

另外,rdtsc 通过X86 指令读取CPU cycle 计数器,如下

#define rdtsc(u64) {                                    \
    uint32_t hi, lo;                                    \
    __asm__ __volatile__ ("RDTSC\n\t" : "=a" (lo), "=d" (hi)); \
    u64 = ((uint64_t )hi << 32) | lo;                        \
}

结果显示,实际运行的CPU cycles(end-start) 和变量cycles 非常接近。

通过计算最大IPC(Instruction Per Cycle)得到CPU 指令集并行数

单核CPU 在每个时钟周期可执行N 条指令,通过计算一个程序最大的IPC 即可(向上取整)近似得到N 的大小。基本思路是将(可并行的)M(>N)条指令同时执行,得到的IPC。

    int cycles[8] = {NUM, NUM, NUM, NUM, NUM, NUM, NUM, NUM};
    
    rdtsc(start);
    __asm volatile(
    "cyclemeasure8:\n"
    "    dec %[counter] \n"
    "    dec %[counter2] \n"
    "    dec %[counter3] \n"
    "    dec %[counter4] \n"
    "    dec %[counter5] \n"
    "    dec %[counter6] \n"
    "    dec %[counter7] \n"
    "    dec %[counter8] \n"
    "    jnz cyclemeasure8 \n"
    : /* read/write reg */ [counter] "+r"(cycles[0]), 
    [counter2] "+r"(cycles[1]),
    [counter3] "+r"(cycles[2]),
    [counter4] "+r"(cycles[3]),
    [counter5] "+r"(cycles[4]),
    [counter6] "+r"(cycles[5]),
    [counter7] "+r"(cycles[6]),
    [counter8] "+r"(cycles[7])
    );  
    rdtsc(end);

    printf("IPC             : %lf\n", (8.0*NUM)/(end-start));

注意,一般N 的大小不会大于通用寄存器个数。

两个问题

1 尝试将循环中指令修改为nop,但效果不如计算(inc/dec 无依赖关系的数据)好;

2 计算和IO (load/store)相关的指令执行器应该是不同的,它们之间的并行是不是使得理论IPC 应稍大于N?

调度和中断

调度和中断是运行时对结果最大两个因素:1 调度可通过设置进程优先级为SCHED_FIFO完成;2 中断在X86 系统中可通过cli/sti 指令关闭和开启(需要root 权限和IO 权限,即iopl(3))。

通过软件的方法多次测试,去除掉因为调度或中断导致的明显偏差测试也是一种方法,具有更好兼容性。

参考文献

https://lemire.me/blog/2019/05/19/measuring-the-system-clock-frequency-using-loops-intel-and-arm/

https://en.wikipedia.org/wiki/Superscalar_processor

Some Linux Commands for Backups and Restores

The machine that hosts the site(http://blog.foool.net) collapsed some time ago, and this is not the first time that such a breakdown happens. I reinstalled the machine and restored the data, including the database and some ordinary files. The followings are some useful commands that helped me to backup and restore the system.

Disk Info.

df -h

list info. of all file systems. (‘h’ indicates to print the size with the human-readable format )

lsblk

list info. of all block devices.

File info.

du -a / | sort -nr | head -10

list the top 10 largest directories and files.

-a counts all files, not just directories.

-n compare according to string numerical value.

-r reverse the results.

-<num> list only the top <num> items.

du -sh

the storage amount of the current directory.

-s display only a total result

find . -type f -printf "%s %p\n" | sort -nr | head -10

list the top 10 biggest files of the current directory.

Backup & Restore

ssh user@remote "dd if=/dev/sda | gzip -1 -" | dd of=image.gz

Backup disk /dev/sda to a remote compressed image file.

The second – of “gzip -1 -” means reading the input from standard input.

ssh user@ip ‘dd if=/home/user/sdb.img.gz’ | gunzip -1 - | dd of=/dev/sdb

Restore disk /dev/sdb from a remote compressed image file.

sudo rsync -aAXv / –-delete --exclude={/dev/*,/proc/*,/lost+found}  user@ip:path

Backup all files, excluding some specific ones, to remote.

-aAXv (a)archive mode, (A)perserve ACLs, (X)preserve extended attributes, (v)verbose.

在Linux 中测量延迟

原文地址:http://btorpey.github.io/blog/2014/02/18/clock-sources-in-linux/

他山之石,可以攻玉,该文章将详细地介绍了在Linux 系统中使用TSC 时钟精确地计算一段代码过程(中断和函数等)执行时间需要注意的内容,可以配置Intel 官方文档《How to Benchmark Code Execution Times on Intel® IA-32 and IA-64 Instruction Set Architectures》一起阅读。下面是译文。

为了在现代(操作)系统中的测量(一段过程的)延迟,我们需要(测量时间间隔的时钟)至少能够以微秒为单位,最好以纳秒为单位。好消息是,使用相对现代的硬件和软件,可以准确地测量小到几个纳秒的时间间隔。

但是,为确保您的(测量)结果是准确的,更重要的是要了解您要测量什么,以及不同情况下测量过程的边界是什么。

概要

为了(在Linux 中测量延迟)获得最佳效果,您应该使用:

  • (使用)Linux 内核 2.6.18 或更高版本 —— 这是第一个包含 hrtimers (高精度时钟)包的版本。最好的是 2.6.32 或更高版本,包括了对大多数不同时钟源的支持。
  • 具有恒定不变的 TSC(constant, invariant TSC (time-stamp counter))的 CPU。这意味着 TSC 在所有插槽和CPU核心上都以恒定速率运行,而不管电源管理(代码)对 CPU 的频率进行如何更改。如果 CPU 支持 RDTSCP 指令那就更好了(RDTSCP 会使得读取的时间更准确和稳定)。
  • TSC 应配置为Linux 内核的时钟源。
  • 您应该测量发生在同一台机器上的两个事件之间的间隔(机器内计时,即intra-machine timing)。
  • 对于机器内计时,您最好的选择使用汇编语言直接读取 TSC。在我的测试机器上,软件读取 TSC 大约需要 100ns,这是该方法准确性的(边界)限制(要测量100ns 以内的时间间隔是不实际的)。但不同机器读取TSC 开销不尽相同,这就是为什么我提供了源代码,您可以用来进行测量自己的机器。
    • 请注意,上面提到的100ns 主要是因为我的Linux 机器不支持RDTSCP 指令。所以为了获取合理准确的计时,在RDTSC 之前还执行了CPUID 指令以序列化指令执行过程。而在另一台支持RDTSCP 指令的机器(新 MacBook Air)上,开销下降了大约 14ns。

下面将讨论时钟在Linux 上的工作原理,如何从软件(角度)访问各种时钟,以及如何测量访问它们的开销。

继续阅读

利用autossh和中间主机为内网主机建立稳定ssh 连接

通常会遇到一些内网主机没有独立IP 地址,隐藏在NAT 之后,用户无法直接建立ssh 连接。

这时候就需要一个中间人机器(具有独立IP)做为跳板,内网机器反向连接至中间机器。用户登陆时,首先连接至中间机器,再反向连接至内网主机。

其步骤如下:

  1. 在内网主机,运行 ssh -R 7777:localhost:22 qing@middleman
  2. 在中间主机,运行 ssh -p 7777 user@localhost

注意:步骤2的user 是内网主机user。

ssh -R 参数中7777 是远端映射的端口,连接该端口将建立起和内网22 号端口的链接;下面是man ssh 中关于-R 选项的说明

-R [bind_address:]port:host:hostport

-R [bind_address:]port:local_socket

-R remote_socket:host:hostport

-R remote_socket:local_socket

-R [bind_address:]port

Specifies that connections to the given TCP port or Unix socket on the remote (server) host are to be forwarded to the local side.

This works by allocating a socket to listen to either a TCP port or to a Unix socket on the remote side. Whenever a connection is made to this port or Unix socket, the connection is forwarded over the secure channel, and a connection is made from the local machine to either an explicit destination specified by host port hostport, or local_socket, or, if no explicit destination was specified, ssh will act as a SOCKS 4/5 proxy and forward connections to the destinations requested by the remote SOCKS client. Port forwardings can also be specified in the configuration file. Privileged ports can be forwarded only when logging in as root on the remote machine. IPv6 ad‐ dresses can be specified by enclosing the address in square brackets.

By default, TCP listening sockets on the server will be bound to the loopback interface only. This may be overridden by specifying a bind_address. An empty bind_address, or the address ‘*’, indicates that the remote socket should listen on all interfaces. Specifying a remote bind_address will only succeed if the server's GatewayPorts option is enabled (see sshd_config(5)).

If the port argument is ‘0’, the listen port will be dynamically allocated on the server and reported to the client at run time. When used together with -O forward the allocated port will be printed to the standard output.

但这样存在两个问题:1)ssh 连接超过固定时间会自动释放;2)每次连接中间机器都需要用户手动输入密码。

第一个问题通过autossh 解决

autossh 通过将ssh 命令包裹至一个循环中,并在ssh 命令断开时自动建立连接,这样就保证了即使内网机器无法访问,也会自动建立和中间主机的逆向连接。autossh 命令格式如下

autossh [autossh options] [ssh options]

即autossh 除了自身参数,其他参数直接用ssh 的即可。

第二个问题通过公钥免密码登录解决:1)内网主机执行ssh-keygen;2)ssh-copy-id -i ~/.ssh/id_rsa.pub user@middleman_machine

结合起autossh 和免密码登录,autossh 命令如下:

autossh -o "PasswordAuthentication=no" -o "PubkeyAuthentication=yes" -i ~/.ssh/id_rsa -R 7777:localhost:22 user@middleman

将该命令添加至开机启动模块中实现开机启动。

Futex 简述

简介:futex 全称为Fast User-space Mutex,是Linux 2.5.7 内核引入的锁原语,不同于其他进程间通信IPC原语(如信号量Semaphore、信号Signal和各种锁pthread_mutex_lock),futex更轻量级、快速,一般应用开发人员可能很少用到,但可基于futex实现各类读写锁、屏障(barriers)和信号机制等。

相关背景

在Linux的早期版本(内核Linux 2.5.7 版本以前),进程间通信(Inter-Process Communication,IPC)沿用的是传统Unix系统和System V 的IPC,如信号量(Semaphores)和Socket 等,这些IPC 均基于系统调用(System Call)。这类方法的缺点是当系统竞争度较低时,每次都进行系统调用,会造成较大系统开销。

原理和做法

用户程序每次调用IPC机制都会产生系统调用,程序发生用户态和内核态的切换,futex 的基本思想是竞争态总是很少发生的,只有在竞争态才需要进入内核,否则在用户态即可完成。futex的两个目标是:1)尽量避免系统调用;2)避免不必要的上下文切换(导致的TLB失效等)。

具体而言,任务获取一个futex 将发起带锁的减指令,并验证数值结果值是否为0(加上了锁),如果成功则可继续执行程序,失败(为已经占用的锁继续加锁)则任务在内核被阻塞。为相同futex 变量的加锁的任务被阻塞后放在同一个队列,解锁任务通过减少变量(只有一个加锁且锁队列为空)或进入内核从锁队列唤醒任务。

注意:futex 在Linux 的内核实现为一个系统调用(SYS_futex),用户程序如果直接调用它肯定会进入内核态,它还需要和其他语句(如原子操作)配合使用,新手在未理解其futex 原理和并发控制机制时极易犯错,这也是为什么不推荐直接使用它的原因。

继续阅读

记一次有趣的Bug – 返回值被截断为32位

上周修改了hdrt 库文件中头文件的include 关系,结果出现了一个有趣的bug。具体表现为:当函数foo() 调用某个函数fun_called(),返回的值ret_val 总是从64位被截断为32位。

首先,在foo() 函数头和func_called 函数尾输出ret_val,确认是返回值是在调用过程中被截断了。

其次,在gdb 中查看生成的代码,发现ret_val所存储的寄存器rax (返回值一般存储在rax寄存器中)在被返回时,被cltq 指令截断了高32 位。

  0x555555565adb <foo+42> callq 0x555555569874<fun_called>
  0x555555565ae0 <foo+47> cltq
  0x555555565ae2 <foo+49> mov %rax,-0x8(%rbp)
  0x555555565ae6 <foo+53> cmpq $0x0,-0x8(%rbp)
  0x555555565aeb <foo+58> jne 0x555555565b2e <foo+125>

最后,查了相关资料https://stackoverflow.com/a/26209434/1424948 可能原因是:

1)函数声明(prototype)中没有fun_called 的声明;

2)没有引用包含fun_called 声明的头文件。

默认情况下,调用函数foo 在不知道被调用函数fun_called 的返回值类型的情况下,会按照int 类型大小的值处理,即32位。

对于这个bug 其实有两个建议:

1 非特殊情况不要用强制类型转换(即type cast),(char *)这类转换会屏蔽很多暴露问题的warning;

2 要查看/消除warning,例如这个bug 实际隐藏在了warning 中,因为返回值被编译当做默认的int 类型返回时,获取返回值变量不是int 时则告警了类型不匹配。