在x86_64中使用POSIX线程库汇编程序(yasm)需要更多的执行时间

时间:2018-03-25 11:35:53

标签: performance assembly concurrency x86 nasm

我想使用yasm线程(或简称POSIX)库在pthread程序中实现并行处理。

代码

这是我计划中最重要的部分。

section .data
pThreadID1      dq      0
pThreadID2      dq      0
MAX:            dq      100000000
value:          dd      0

section .bss
extern          pthread_create
extern          pthread_join

section .text

global main
main:
    ;create the first thread with id = pThreadID1
    mov     rdi, pThreadID1 
    mov     rsi, NULL
    mov     rdx, thread1
    mov     rcx, NULL
    call    pthread_create
    ;join the 1st thread
    mov     rdi, qword [pThreadID1]
    mov     rsi, NULL
    call    pthread_join 

    ;create the second thread with id = pThreadID2
    mov     rdi, pThreadID2 
    mov     rsi, NULL
    mov     rdx, thread2
    mov     rcx, NULL
    call    pthread_create
    ;join the 2nd thread
    mov     rdi, qword [pThreadID2]
    mov     rsi, NULL
    call    pthread_join
    ;print value block

其中thread1包含value增加MAX/2次的循环:

global thread1
thread1:
    mov     rcx, qword [MAX]
    shr     rcx, 1

    thread1.loop:
        mov     eax, dword [value]
        inc     eax
        mov     dword [value], eax
        loop    thread1.loop

    ret

thread2类似。 注意:thread1thread2 分享变量value

结果

我按如下方式组装和编译上述程序:

yasm -g dwarf2 -f elf64 Parallel.asm -l Parallel.lst
gcc -g Parallel.o -lpthread -o Parallel

然后我使用time命令来了解已执行的执行时间:

time ./Parallel

我得到了

value: +100000000

real    0m0.482s
user    0m0.472s
sys 0m0.000s

问题

行。在上面的程序中,我创建一个线程等待它完成,然后再创建第二个线程。不是最好的"线程",不是吗?所以我按如下方式更改了程序中的顺序:

;create thread1
;create thread2
;join   thread1
;join   thread2

我希望在这种情况下经过的时间会减少,但我得到了

value: +48634696

real    0m2.403s
user    0m4.772s
sys 0m0.000s

我理解为什么value不等于MAX,但我不明白的是为什么在这种情况下经过的时间明显更多?我错过了什么吗?

修改

我决定通过为每个变量使用不同的变量来排除thread1thread2之间的重叠,然后只添加结果。在这种情况下" parallel"订单给出较少的经过时间(与之前的结果相比),但无论如何,大于"系列"顺序。

代码

仅显示更改

数据

现在有两个变量 - 每个线程一个。

section .data
value1:         dd      0
value2:         dd      0

线程

每个线程负责增加自己的值。

global thread1
thread1:
    mov     rcx, qword [MAX]
    shr     rcx, 1

    thread1.loop:
        mov     eax, dword [value1]
        inc     eax
        mov     dword [value1], eax
        loop    thread1.loop

    ret

thread2类似(将1替换为2)。

获得最终结果

假设评论代表问题开头的代码部分的相应代码块,程序如下。

平行订单
;create thread1
;create thread1

;join thread1
;join thread2

mov     eax, dword [value]
add     eax, dword [value1]
add     eax, dword [value2]
mov     dword [value], eax
结果
value: +100000000

Performance counter stats for './Parallel':

   3078.140527      cpu-clock (msec)                                            

   1.586070821 seconds time elapsed
系列订单
;create thread1
;join thread1

;create thread2
;join thread2

mov     eax, dword [value]
add     eax, dword [value1]
add     eax, dword [value2]
mov     dword [value], eax
结果
value: +100000000

Performance counter stats for './Parallel':

    508.757321      cpu-clock (msec)                                            

   0.509709406 seconds time elapsed

更新

我已经绘制了一个简单的图表,该图表反映了针对4种不同模式的MAX值的时间依赖性。 enter image description here

1 个答案:

答案 0 :(得分:2)

一次运行两个线程的版本速度较慢,因为运行代码的两个核心将在包含计数器值的缓存行上竞争。高速缓存行必须在两个内核之间来回移动,每次需要10个周期,并且在它移回另一个内核之前只会发生几个增量。将其与单线程情况进行比较,其中每5个周期 1 可以发生一次增量,受到存储转发延迟和慢value1指令的限制。

对于线程增加共享值的情况和它们不同的其他情况都是如此。它甚至适用于后一种情况,因为声明值value2和{{1}}占据内存中的连续位置,因此出现在同一缓存行中。由于一致性发生在缓存行粒度,这种所谓的“错误共享”#34;效果类似于真实共享(第一种情况)。

您提到虽然真假共享案例都比单线程案例慢得多,但真正的共享案例仍然比虚假共享案例慢。在没有测试的情况下,我曾经想过,这两个案例在性能方面是相同的,但它们没有意义:虽然两者都遭受上述缓存行颠簸,但真正的共享案例可能还会遭受额外的内存顺序清除 - 虽然确切的机制尚不清楚。