带有示例的Linux代码性能测试

当我开始学习Java时,我尝试解决的第一个任务就是确定偶数/奇数。我知道执行此操作的几种方法,但决定在Internet上寻找“正确”的方法。找到的所有链接上的信息告诉我有关x%2形式的唯一正确解,以便获得除法的余数。如果余数为0,则数字为偶数;如果余数为1,则为奇数。

从ZX Spectrum时代起,我就想起了另一种方式,它与二进制系统中数字的表示形式相关。十进制中的任何数字都可以写成2的幂的和。例如,对于一个字节,即8位,十进制系统中的任何数字都可以表示为数字128 + 64 + 32 + 16 + 8 + 4 + 2 + 1的总和。

这只是两个幂的序列。将数字转换为二进制系统时,如果需要考虑数字,则在二进制表示形式中将为1,如果不需要,则为0。

例如:

10 = 1010(8 + 0 + 2 + 0)
13 = 1101(8 + 4) + 0 + 1)
200 = 11001000(128 + 64 + 0 + 0 + 8 + 0 + 0 + 0)

您可以立即注意到一个事实,即奇数只能给出值为1的2的零次幂,根据定义,所有其他2的幂将为偶数。这自动意味着,从二进制数系统的角度来看,如果我们要检查任何数字以进行奇偶校验,则无论它有多大,都不需要检查整数。我们只需要检查第一位(最右边)。如果为0,则数字为偶数,因为所有其他位都为偶数,反之亦然,如果最右边的位为1,则保证数字为奇数,因为所有其他位仅给出偶数。
要仅检查数字中的正确位,可以使用多种方法。其中之一是二进制AND。


二进制AND(AND)按照以下规则工作。如果您申请任何数字,我们将其称为原始的逻辑与(与数字0进行逻辑与),那么这种运算的结果始终为0。这样,您就可以将不需要的位清零。如果您申请原件1,那么您将获得原件。

在二进制系统中,很容易这样写:

0 AND 0 = 0(原始零)
1 AND 0 = 0(原始零)
0 AND 1 = 0(不改变原始)
1 AND 1 = 1(不改变原始)

从这里开始很简单规则。

如果将所有单位的AND运算应用于所有数字(所有位均打开),我们将获得相同的初始数字。

如果将所有零的AND应用于任何数字(所有位均关闭),则得到0。

例如:

如果我们将和0至字节13,那么我们将得到0在十进制它看起来像13和0 = 0

如果我们将和0至字节200,我们将得到0,或写下200和0 = 0简要介绍。
同样是相反的,应用于13所有包含的位,对于一个字节,它将是8个单位,我们得到了原始的。在二进制系统

00001101和11111111 = 00001101 或在十进制系统13 AND 255 = 13中,对于200,分别为11001000 AND 11111111 = 11001000,或者在十进制系统200 AND 255 = 200中

二进制验证


要检查奇偶校验号,我们只需要检查最右边的位。如果为0,则数字为偶数;如果为1,则数字为偶数。知道使用AND可以使某些位保持原始状态,而有些则可以重置,因此我们可以重置除最右边一位以外的所有位。例如:

二进制系统中的13是1101。让我们对它应用AND 0001(我们重置所有位,最后一位保留原始位)。在1101年,我们将除最后一位之外的所有位都更改为0,得到0001。我们仅从原始数字中获得了最后一位。在十进制系统中,它将看起来像13 AND 1 = 1。

与数字200相同的东西,二进制格式为11001000。我们将AND 00000001应用到它,根据相同的方案,将所有位清零,保留最后一位不变,得到00000000,然后用AND重置左7个零,得到最后0从原始号码开始。在十进制系统中,看起来像200 AND 1 =0。

因此,将AND 1命令应用于任何数字,我们将得到0或1。如果结果为0,则该数字为偶数。为1时,数字为奇数。

在Java中,二进制AND表示为&。因此,200&1 = 0(偶数)和13&1 = 1(奇数)。

这意味着至少有两种确定偶数的方法。

X%2-通过除以两个
X&1 的余数-通过二进制AND

诸如OR,AND,XOR之类的二进制运算由处理器在最短的时间内处理。但是除法运算是一项艰巨的任务,为了执行除法运算,处理器需要处理大量指令,从根本上执行整个程序。但是,存在二进制左移和右移运算,例如,可以将数字快速除以2。问题是编译器是否使用此优化,以及这两个比较之间是否存在差异,实际上它们是相同的。

编码


我们将编写一个程序,该程序将按周期处理9,000,000,000个数字,并通过确定除法的余数来确定它们属于偶数/奇数。

public class OddEvenViaMod {
        public static void main (String[] args) {
                long i=0;
                long odds=0;
                long evens=0;
                do {
                if ((i % 2) == 0) {
                        evens++;
                        }
                else {
                        odds++;
                        }
                i++;
                } while (i<9000000000L);
                System.out.println("Odd " + odds);
                System.out.println("Even " + evens);
        }
}

我们将编写完全相同的内容,但实际上会更改两个字符,并通过二进制AND来检查同一件事。

public class OddEvenViaAnd {
        public static void main (String[] args) {
                long i=0;
                long odds=0;
                long evens=0;
                do {
                if ((i & 1) == 0) {
                        evens++;
                        }
                else {
                        odds++;
                        }
                i++;
                } while (i<9000000000L);
                System.out.println("Odd " + odds);
                System.out.println("Even " + evens);

现在我们需要以某种方式比较这两个程序。

Linux上的资源。中央处理器


创建任何操作系统都花费了大量时间,特别是在程序之间公平分配资源上。一方面,这很好,因为可以运行两个程序,因此可以确保它们可以并行工作,但是,另一方面,当您需要检查程序的性能时,非常有必要限制或至少减少其他程序对程序的外部影响。程序和操作系统。

首先要弄清楚的是处理器。Linux OS为每个进程存储一个位掩码,该位掩码指示应用程序可以使用哪些内核,哪些不能。您可以使用taskset命令查看和更改此掩码。

例如,让我们看看处理器中的内核数量:

[user@localhost]# grep -c processor /proc/cpuinfo
4

我的计算机具有4核处理器。这很好,因为我将根据自己的需求分配其中之一。

让我们看看当前是否所有这些都与top命令一起使用:

[user@localhost]# top

按“ 1”可分别查看每个内核的信息:

top - 13:44:11 up 1 day, 23:26,  7 users,  load average: 1.48, 2.21, 2.02
Tasks: 321 total,   1 running, 320 sleeping,   0 stopped,   0 zombie
%Cpu0  :  7.7 us,  6.8 sy,  0.0 ni, 85.5 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  :  9.2 us,  4.2 sy,  0.0 ni, 86.6 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu2  :  7.6 us,  3.4 sy,  0.0 ni, 89.1 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu3  :  8.4 us,  4.2 sy,  0.0 ni, 87.4 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 16210820 total,   296972 free, 10072092 used,  5841756 buff/cache
KiB Swap: 16777212 total, 16777212 free,        0 used.  5480568 avail Mem
....

在这里,我们看到所有内核的使用大致相同。(每个核心的us和sy和id指示器大致相等)。

现在,让我们尝试使用taskset命令查看相同的内容。

[user@localhost]# taskset -p 1
pid 1's current affinity mask: f

十六进制系统中的位掩码“ F”表示十进制为15,二进制为1111(8 + 4 + 2 +1)。所有位均启用,这意味着具有PID 1的进程将使用所有内核。
在Linux上,当一个进程通过克隆系统调用生成另一个进程时,将在克隆时从父进程复制位掩码。这意味着,如果我们为初始化进程更改此掩码(在我的情况下为systemd),则在通过systemd启动任何新进程时,该新进程将已经使用新掩码启动。

您可以使用相同的命令更改进程的掩码,列出要保留给进程使用的CPU内核数。假设我们要为进程保留内核0.2.3,并且要为systemd进程禁用内核1。为此,我们需要运行以下命令:

[user@localhost]#  taskset -pc 0,2,3 1
pid 1's current affinity list: 0-3
pid 1's new affinity list: 0,2,3

我们检查:

[user@localhost]# taskset -p 1
pid 1's current affinity mask: d

掩码以十六进制表示法更改为“ D”,即十进制为13,二进制为1101(8 + 4 + 0 + 1)。

从现在开始,将由systemd进程克隆的任何进程将自动具有CPU使用率的掩码1101,这意味着将不使用内核号1。

我们禁止在所有进程中使用内核


阻止主Linux进程使用单个内核只会影响此进程创建的新进程。但是在我的系统中,已经不存在一个进程,而是一个完整的进程,例如crond,sshd,bash等。如果我需要禁止所有进程使用一个内核,那么必须为每个正在运行的进程运行tasket命令。

为了获得所有进程的列表,我们将使用内核提供给我们的API,即/ proc文件系统。

在循环的更深处,我们查看每个正在运行的进程的PID,并为其和所有线程更改掩码:

[user@localhost]# cd /proc; for i in `ls -d [0-9]*`; do taskset -a -pc 0,2,3 $i; done
pid 1's current affinity list: 0,2,3
pid 1's new affinity list: 0,2,3
...

由于在程序执行期间,某些进程可能有时间生成其他进程,因此最好多次运行此命令。

使用top命令检查我们的工作结果:

[user@localhost]# top
top - 14:20:46 up 2 days, 3 min,  7 users,  load average: 0.19, 0.27, 0.57
Tasks: 324 total,   4 running, 320 sleeping,   0 stopped,   0 zombie
%Cpu0  :  8.9 us,  7.7 sy,  0.0 ni, 83.4 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  :  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu2  :  9.5 us,  6.0 sy,  0.0 ni, 84.5 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu3  :  8.4 us,  6.6 sy,  0.0 ni, 85.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 16210820 total,   285724 free, 10142548 used,  5782548 buff/cache
KiB Swap: 16777212 total, 16777212 free,        0 used.  5399648 avail Mem

如您所见,情况有所改变,现在对于内核0.2.3,我们的参数us,sy,id平均相同,对于内核1,我们在用户空间和sys中的内核消耗为0,内核处于100%空闲(空闲100) )我们的应用程序不再使用内核1,并且内核当前仅使用很小的一部分。

现在,将性能测试的任务减少到在自由内核上启动我们的过程。

记忆


分配给进程的物理内存可以很容易地从任何进程中获取。这种机制称为交换。如果Linux有交换的地方,它仍然会这样做。像其他任何进程一样,阻止操作系统从我们的进程中获取内存的唯一方法是完全禁用交换部分,我们将这样做:

[user@localhost]$ sudo swapoff -a
[user@localhost]$ free -m
              total        used        free      shared  buff/cache   available
Mem:          15830        7294        1894         360        6641        7746
Swap:             0           0           0

我们分配了1个未使用的处理器内核,并且还删除了从Linux内核交换内存的功能。

磁碟


为了减少磁盘对启动过程的影响,请在内存中创建磁盘,然后将所有必需的文件复制到该磁盘上。

创建目录并挂载文件系统:

[user@localhost]$ sudo mkdir /mnt/ramdisk;
[user@localhost]$ mount -t tmpfs -o size=512m tmpfs /mnt/ramdisk
[user@localhost]$ chown user: /mnt/ramdisk

现在,我们需要弄清楚计划如何以及如何启动它。为了运行我们的程序,我们首先需要编译我们的代码:

[user@localhost]$ javac OddEvenViaMod.java

然后,您需要运行它:

[user@localhost]$ java OddEvenViaMod

但是在我们的例子中,我们希望在处理器内核上运行该进程,而该进程没有被其他进程使用。因此,通过任务集运行它:

[user@localhost]# taskset -c 1 time java OddEvenViaMod

在我们的测试中,我们需要测量时间,因此我们的发射线变成

taskset -c 1 time java OddEvenViaMod

Linux OS支持多种格式的可执行文件,其中最常见的是ELF格式。此文件格式使您可以告诉操作系统不要运行您的文件,而是运行另一个文件。乍一看,这听起来不是很合逻辑且易于理解。想象一下,我启动了Minesweeper游戏,然后Mario游戏为我启动-它看起来像病毒。但这是逻辑。如果我的程序需要某种动态库,例如libc或任何其他动态库,则这意味着OS必须首先将该库加载到内存中,然后再加载并运行我的程序。在操作系统本身中放置此类功能似乎是合乎逻辑的,但是操作系统在内存的受保护区域中工作,并且应包含尽可能少的必要功能。因此,ELF格式使您有机会告诉OS我们要下载其他程序,此“其他”程序将下载所有必需的库和我们的程序,并开始整个过程​​。

因此,我们需要运行3个文件,这是taskset,time,java。

检查其中的第一个:

[user@localhost]$ whereis taskset
taskset: /usr/bin/taskset /usr/share/man/man1/taskset.1.gz

Bash将运行文件/ usr / bin / taskset,检查其中的内容:

[user@localhost]$ file /usr/bin/taskset
/usr/bin/taskset: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=7a2fd0779f64aa9047faa00f498042f0f0c5dc60, stripped

这是我上面写过的ELF文件。在ELF文件中,除了程序本身之外,还有各种头文件。通过启动该文件,操作系统检查其头,如果文件中存在“请求程序解释器”头,则操作系统将从该头启动文件,并将初始启动的文件作为参数传递。

检查此标头是否存在于我们的ELF文件中:

[user@localhost]$ readelf -a /usr/bin/taskset  | grep -i interpreter
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

标头存在,这意味着通过启动/ usr / bin / taskset文件,我们实际上运行了/lib64/ld-linux-x86-64.so.2。

检查此文件是什么:

[user@localhost]$ ls -lah /lib64/ld-linux-x86-64.so.2
lrwxrwxrwx 1 root root 10 May 21  2019 /lib64/ld-linux-x86-64.so.2 -> ld-2.17.so

这是指向文件/lib64/ld-2.17.so的Sim链接。一探究竟:

[user@localhost]$ file /lib64/ld-2.17.so
/lib64/ld-2.17.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=a527fe72908703c5972ae384e78d1850d1881ee7, not stripped

如您所见,这是操作系统将运行的另一个ELF文件。我们看一下标题:

[user@localhost]$ readelf -a /lib64/ld-2.17.so  | grep -i interpreter
[user@localhost]$

我们看到该ELF文件没有这样的头,因此OS将运行该文件并将控制权转移给它。并且此文件已经可以打开我们的文件/ usr / bin / taskset,从那里读取所有必需库的信息。所需库的列表也位于ELF文件的标题中。我们可以使用ldd或readelf命令查看此列表,这是同一件事:

[user@localhost]$ ldd /usr/bin/taskset
	linux-vdso.so.1 =>  (0x00007ffc4c1df000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f4a24c4e000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f4a2501b000)

[user@localhost]$ readelf -a /usr/bin/taskset  | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

VDSO是与库无关的链接的内存,因此ELF文件中缺少它作为必需的库。

这清楚地表明程序/lib64/ld-2.17.so负责运行需要它的所有程序,而这些程序都是具有动态链接库的程序。

如果运行/ usr / bin / taskset,则与使用/ usr / bin / taskset参数运行/lib64/ld-2.17.so完全相同。

我们回到磁盘对测试的影响问题。现在我们知道,如果要从内存中加载程序,则不需要复制一个文件,而是复制几个文件:

[user@localhost]$ cp /lib64/libc-2.17.so /mnt/ramdisk/
[user@localhost]$ cp /lib64/ld-2.17.so /mnt/ramdisk/
[user@localhost]$ cp /usr/bin/taskset /mnt/ramdisk/

我们所做的时间相同,它们的库要求完全相同(我们已经复制了ld和libc)。

[user@localhost]$ cp /usr/bin/time /mnt/ramdisk/

对于Java来说,事情要复杂一些,因为Java需要许多可以长时间复制的不同库。为了简化我的生活,我将整个目录从java openjdk复制到内存中的磁盘上并创建一个sim链接。当然,在这种情况下,将保留磁盘访问权限,但是访问次数会更少。

[user@localhost]$ cp -R /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.222.b10-0.el7_6.x86_64 /mnt/ramdisk/

重命名旧目录,在其末尾添加.default

[user@localhost]$ sudo mv /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.222.b10-0.el7_6.x86_64{,.default}

并创建一个符号链接:

[user@localhost]$ sudo ln -s /mnt/ramdisk/java-1.8.0-openjdk-1.8.0.222.b10-0.el7_6.x86_64 /usr/lib/jvm/

我们已经知道如何通过/lib64/ld-2.17.so文件的参数运行一个二进制文件,该文件实际上会启动。但是,如何使程序/lib64/ld-2.17.so从指定的目录中加载已加载的库?帮助我们的ld,我们从中了解到,如果您声明环境变量LD_LIBRARY_PATH,则ld程序将从我们指定的目录中加载库。现在,我们拥有所有数据,准备了Java应用程序的启动行。

我们连续开始几次并检查:

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaMod
Odd 4500000000
Even 4500000000
10.66user 0.01system 0:10.67elapsed 99%CPU (0avgtext+0avgdata 20344maxresident)k
0inputs+64outputs (0major+5229minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaMod
Odd 4500000000
Even 4500000000
10.65user 0.01system 0:10.67elapsed 99%CPU (0avgtext+0avgdata 20348maxresident)k
0inputs+64outputs (0major+5229minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaMod
Odd 4500000000
Even 4500000000
10.66user 0.01system 0:10.68elapsed 99%CPU (0avgtext+0avgdata 20348maxresident)k
0inputs+64outputs (0major+5229minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaMod
Odd 4500000000
Even 4500000000
10.65user 0.01system 0:10.67elapsed 99%CPU (0avgtext+0avgdata 20348maxresident)k
0inputs+96outputs (0major+5234minor)pagefaults 0swaps
[user@localhost ramdisk]$

在程序执行过程中,我们可以运行在最顶层,并确保程序在正确的CPU内核上运行。

[user@localhost ramdisk]$ top
...
%Cpu0  : 19.7 us, 11.7 sy,  0.0 ni, 68.6 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  :100.0 us,  0.0 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu2  :  9.8 us,  9.1 sy,  0.0 ni, 81.1 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu3  : 14.0 us,  9.0 sy,  0.0 ni, 77.1 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 s
...

如您所见,大多数情况下的结果是相似的。不幸的是,我们无法完全消除操作系统对CPU内核的影响,因此结果仍然取决于启动时Linux内核内部的特定任务。因此,最好使用多个开始值的中位数。

在我们的例子中,我们看到一个程序在一个CPU内核上以10.65秒的间隔通过剩余部分对Java程序进行了9,000,000,000个奇偶校验处理。

让我们对第二个程序进行相同的测试,该程序通过二进制AND进行相同的操作。

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaAnd
Odd 4500000000
Even 4500000000
4.02user 0.00system 0:04.03elapsed 99%CPU (0avgtext+0avgdata 20224maxresident)k
0inputs+64outputs (0major+5197minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaAnd
Odd 4500000000
Even 4500000000
4.01user 0.00system 0:04.03elapsed 99%CPU (0avgtext+0avgdata 20228maxresident)k
0inputs+64outputs (0major+5199minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaAnd
Odd 4500000000
Even 4500000000
4.01user 0.01system 0:04.03elapsed 99%CPU (0avgtext+0avgdata 20224maxresident)k
0inputs+64outputs (0major+5198minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaAnd
Odd 4500000000
Even 4500000000
4.02user 0.00system 0:04.03elapsed 99%CPU (0avgtext+0avgdata 20224maxresident)k
0inputs+64outputs (0major+5198minor)pagefaults 0swaps

现在我们可以放心地说,通过二进制AND进行奇偶校验的比较需要4.02秒,这意味着与通过除法其余部分进行检查相比,它的工作速度是2.6倍,至少在openjdk版本1.8.0上是如此。

Oracle Java与Openjdk


我下载并从Oracle网站的/mnt/ramdisk/jdk-13.0.2目录解压JAVA JDK。

编译:

[user@localhost ramdisk]$ /mnt/ramdisk/jdk-13.0.2/bin/javac OddEvenViaAnd.java

我们推出:

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/jdk-13.0.2/bin/java OddEvenViaAnd
Odd 4500000000
Even 4500000000
10.39user 0.01system 0:10.41elapsed 99%CPU (0avgtext+0avgdata 24260maxresident)k
0inputs+64outputs (0major+6979minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/jdk-13.0.2/bin/java OddEvenViaAnd
Odd 4500000000
Even 4500000000
10.40user 0.01system 0:10.42elapsed 99%CPU (0avgtext+0avgdata 24268maxresident)k
0inputs+64outputs (0major+6985minor)pagefaults 0swaps

我们编译第二个程序:

[user@localhost ramdisk]$ /mnt/ramdisk/jdk-13.0.2/bin/javac OddEvenViaMod.java

我们推出:

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/jdk-13.0.2/bin/java OddEvenViaMod
Odd 4500000000
Even 4500000000
10.39user 0.01system 0:10.40elapsed 99%CPU (0avgtext+0avgdata 24324maxresident)k
0inputs+96outputs (0major+7003minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/jdk-13.0.2/bin/java OddEvenViaMod
Odd 4500000000
Even 4500000000
10.40user 0.00system 0:10.41elapsed 99%CPU (0avgtext+0avgdata 24316maxresident)k
0inputs+64outputs (0major+6992minor)pagefaults 0swaps

在除法其余部分和二进制AND中,oracle jdk中相同源的执行时间相同,这看起来很正常,但是这次同样糟糕,这在除法其余部分的openjdk中已显示。

蟒蛇


让我们尝试在Python中进行比较。首先,该选项除以2:

odd=0
even=0
for i in xrange(100000000):
	if i % 2 == 0:
		even += 1
	else:
		odd += 1
print "even", even
print "odd", odd

我们推出:

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_mod.py
even 50000000
odd 50000000
11.69user 0.00system 0:11.69elapsed 99%CPU (0avgtext+0avgdata 4584maxresident)k
0inputs+0outputs (0major+1220minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_mod.py
even 50000000
odd 50000000
11.67user 0.00system 0:11.67elapsed 99%CPU (0avgtext+0avgdata 4584maxresident)k
0inputs+0outputs (0major+1220minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_mod.py
even 50000000
odd 50000000
11.66user 0.00system 0:11.66elapsed 99%CPU (0avgtext+0avgdata 4584maxresident)k
0inputs+0outputs (0major+1220minor)pagefaults 0swaps

现在使用二进制AND同样的事情:

odd=0
even=0
for i in xrange(100000000):
	if i & 1 == 0:
		even += 1
	else:
		odd += 1
print "even", even
print "odd", odd

我们推出:

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_and.py
even 50000000
odd 50000000
10.41user 0.00system 0:10.41elapsed 99%CPU (0avgtext+0avgdata 4588maxresident)k
0inputs+0outputs (0major+1221minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_and.py
even 50000000
odd 50000000
10.43user 0.00system 0:10.43elapsed 99%CPU (0avgtext+0avgdata 4588maxresident)k
0inputs+0outputs (0major+1222minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_and.py
even 50000000
odd 50000000
10.43user 0.00system 0:10.43elapsed 99%CPU (0avgtext+0avgdata 4584maxresident)k
0inputs+0outputs (0major+1222minor)pagefaults 0swaps

结果表明,AND更快。

在Internet上,已经多次编写了Python中的全局变量比较慢的说法。我决定将最后一个程序的执行时间与AND进行比较,并将其完全相同,但包装在一个函数中:

def main():
	odd=0
	even=0
	for i in xrange(100000000):
		if i & 1 == 0:
			even += 1
		else:
			odd += 1
	print "even", even
	print "odd", odd

main()

在函数中运行:

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_and_func.py
even 50000000
odd 50000000
5.08user 0.00system 0:05.08elapsed 99%CPU (0avgtext+0avgdata 4592maxresident)k
0inputs+0outputs (0major+1222minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_and_func.py
even 50000000
odd 50000000
5.08user 0.00system 0:05.09elapsed 99%CPU (0avgtext+0avgdata 4592maxresident)k
0inputs+0outputs (0major+1223minor)pagefaults 0swaps

如您所见,在Python中通过二进制AND进行的相同奇偶校验比较在单个CPU内核上约5秒钟内处理了100000000个数字,通过AND不进行函数的相同比较耗时约10秒,而通过除法其余部分进行的不进行函数的比较耗时〜 11秒

为什么函数中的Python程序比没有它的情况下运行得更快的原因已经被多次描述,并且与变量的范围有关。

Python能够将程序反汇编成Python在解释程序时使用的内部函数。让我们看看Python使用odd_and_func.py函数将哪些函数用于变体:

[user@localhost ramdisk]# python
Python 2.7.5 (default, Jun 20 2019, 20:27:34)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-36)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> def main():
...     odd=0
...     even=0
...     for i in xrange(100000000):
...             if i & 1 == 0:
...                     even += 1
...             else:
...                     odd += 1
...     print "even", even
...     print "odd", odd
...
>>> import dis
>>> dis.dis(main)
  2           0 LOAD_CONST               1 (0)
              3 STORE_FAST               0 (odd)

  3           6 LOAD_CONST               1 (0)
              9 STORE_FAST               1 (even)

  4          12 SETUP_LOOP              59 (to 74)
             15 LOAD_GLOBAL              0 (xrange)
             18 LOAD_CONST               2 (100000000)
             21 CALL_FUNCTION            1
             24 GET_ITER
        >>   25 FOR_ITER                45 (to 73)
             28 STORE_FAST               2 (i)

  5          31 LOAD_FAST                2 (i)
             34 LOAD_CONST               3 (1)
             37 BINARY_AND
             38 LOAD_CONST               1 (0)
             41 COMPARE_OP               2 (==)
             44 POP_JUMP_IF_FALSE       60

  6          47 LOAD_FAST                1 (even)
             50 LOAD_CONST               3 (1)
             53 INPLACE_ADD
             54 STORE_FAST               1 (even)
             57 JUMP_ABSOLUTE           25

  8     >>   60 LOAD_FAST                0 (odd)
             63 LOAD_CONST               3 (1)
             66 INPLACE_ADD
             67 STORE_FAST               0 (odd)
             70 JUMP_ABSOLUTE           25
        >>   73 POP_BLOCK

  9     >>   74 LOAD_CONST               4 ('even')
             77 PRINT_ITEM
             78 LOAD_FAST                1 (even)
             81 PRINT_ITEM
             82 PRINT_NEWLINE

 10          83 LOAD_CONST               5 ('odd')
             86 PRINT_ITEM
             87 LOAD_FAST                0 (odd)
             90 PRINT_ITEM
             91 PRINT_NEWLINE
             92 LOAD_CONST               0 (None)
             95 RETURN_VALUE

并在不使用代码的情况下检查该函数:

>>> f=open("odd_and.py","r")
>>> l=f.read()
>>>
>>> l
'odd=0\neven=0\nfor i in xrange(100000000):\n\tif i & 1 == 0:\n\t\teven += 1\n\telse:\n\t\todd += 1\nprint "even", even\nprint "odd", odd\n'
>>> k=compile(l,'l','exec')
>>> k
<code object <module> at 0x7f2bdf39ecb0, file "l", line 1>
>>> dis.dis(k)
  1           0 LOAD_CONST               0 (0)
              3 STORE_NAME               0 (odd)

  2           6 LOAD_CONST               0 (0)
              9 STORE_NAME               1 (even)

  3          12 SETUP_LOOP              59 (to 74)
             15 LOAD_NAME                2 (xrange)
             18 LOAD_CONST               1 (100000000)
             21 CALL_FUNCTION            1
             24 GET_ITER
        >>   25 FOR_ITER                45 (to 73)
             28 STORE_NAME               3 (i)

  4          31 LOAD_NAME                3 (i)
             34 LOAD_CONST               2 (1)
             37 BINARY_AND
             38 LOAD_CONST               0 (0)
             41 COMPARE_OP               2 (==)
             44 POP_JUMP_IF_FALSE       60

  5          47 LOAD_NAME                1 (even)
             50 LOAD_CONST               2 (1)
             53 INPLACE_ADD
             54 STORE_NAME               1 (even)
             57 JUMP_ABSOLUTE           25

  7     >>   60 LOAD_NAME                0 (odd)
             63 LOAD_CONST               2 (1)
             66 INPLACE_ADD
             67 STORE_NAME               0 (odd)
             70 JUMP_ABSOLUTE           25
        >>   73 POP_BLOCK

  8     >>   74 LOAD_CONST               3 ('even')
             77 PRINT_ITEM
             78 LOAD_NAME                1 (even)
             81 PRINT_ITEM
             82 PRINT_NEWLINE

  9          83 LOAD_CONST               4 ('odd')
             86 PRINT_ITEM
             87 LOAD_NAME                0 (odd)
             90 PRINT_ITEM
             91 PRINT_NEWLINE
             92 LOAD_CONST               5 (None)
             95 RETURN_VALUE

如您所见,在带有已声明函数的变体中,Python使用带有FAST后缀的内部函数,例如STORE_FAST,LOAD_FAST,而在未声明函数的变体中,Python使用内部函数STORE_NAME和LOAD_NAME。

本文没有什么实际意义,并且旨在更多地了解Linux和编译器的某些功能。

对所有人都好!

All Articles