Многоядерный в NASM Windows: потоки выполняются случайным образом - PullRequest
0 голосов
/ 03 марта 2019

У меня есть код в NASM (64 бит) в Windows для запуска четырех одновременных потоков (каждый из которых назначен отдельному ядру) на четырехъядерном компьютере с Windows x86-64.

Потоки создаются в цикле.После создания потока он вызывает WaitForMultipleObjects для координации потоков.Вызываемая функция - Test_Function (см. Код ниже).

Каждый поток (ядро) выполняет Test_Function для большого массива.Первое ядро ​​начинается с нуля элемента данных, второе ядро ​​начинается с 1, третье ядро ​​начинается с 2, четвертое ядро ​​начинается с 3, а каждое ядро ​​увеличивается на четыре (например, 0, 4, 8, 12).

В Test_Function я создал небольшую тестовую программу, которая записывает одно из значений входных данных в местоположение, соответствующее его начальному байту, чтобы убедиться, что я успешно создал четыре потока и они возвращают правильные данные.

Каждый поток должен записать значение шага (32), но тест показывает, что четыре поля заполняются случайным образом, причем некоторые поля отображаются как ноль.Если я повторяю тест несколько раз, я вижу, что нет последовательности, для которой поля будут иметь значение 32 (остальные всегда показывают как 0).Это может быть побочным эффектом WaitForMultipleObjects, но я не видел ничего в документах, чтобы подтвердить это.

Кроме того, WaitForMultipleObjects ожидает в ThreadHandles, возвращаемых CreateThread;когда я проверяю массив ThreadHandles, он всегда показывает так: 268444374, 32, 1652, 1584. Только первый элемент выглядит как размер дескриптора, остальные не выглядят как значения дескриптора.

Одна из возможностей состоит в том, что два параметра, передаваемые в стеке, могут находиться не в правильных местах:

mov rax,0
mov [rsp+40],rax            ; use default creation flags
mov rax,[ThreadCount]
mov [rsp+32],rax            ; ThreadID

Согласно документам, ThreadCount должен быть указателем.Когда я меняю строку на mov rax, ThreadCount (значение указателя), программа вылетает.Когда я изменяю его на:

mov rax,0
mov [rsp+32],rax            ; use default creation flags
mov rax,ThreadCount
mov [rsp+40],rax            ; ThreadID

, теперь он надежно обрабатывает первый поток, но не потоки 2-4.

Таким образом, суть в том, что потоки создаются, но они выполняются случайным образом, причем некоторые потоки не выполняются вообще, в произвольном порядке.Когда я изменяю параметры CreateThread (как показано выше), выполняется первый поток, но не потоки 2-4.

Вот тестовый код, показывающий соответствующие части.Если нужен воспроизводимый пример, я могу его подготовить.

Спасибо за любые идеи.

Init_Cores_fn:
; EACH OF THE CORES CALLS Test_Function AND EXECUTES THE WHOLE PROGRAM.  
; WE PASS THE STARTING BYTE (0, 8, 16, 24) AND THE "STRIDE" = NUMBER OF CORES.  
; ON RETURN, WE SYNCHRONIZE ANY DATA.  ON ENTRY TO EACH CORE, SET THE REGISTERS

; Populate the ThreadInfo array with vars to pass
; ThreadInfo: length, startbyte, stride, vars into registers on entry to each core
mov rdi,ThreadInfo
mov rax,ThreadInfoLength
mov [rdi],rax
mov rax,[stride]
mov [rdi+16],rax    ; 8 x number of cores (32 in this example)
; Register Vars
mov [rdi+24],r15
mov [rdi+32],r14
mov [rdi+40],r13
mov [rdi+48],r12
mov [rdi+56],r10

mov rbp,rsp ; preserve caller's stack frame
sub rsp,56 ; Shadow space

; _____

label_0:

mov rdi,ThreadInfo
mov rax,[FirstByte]
mov [rdi+8],rax ; 0, 8, 16, or 24

; _____
; Create Threads

mov rcx,0               ; lpThreadAttributes (Security Attributes)
mov rdx,0               ; dwStackSize
mov r8,Test_Function        ; lpStartAddress (function pointer)
mov r9,ThreadInfo       ; lpParameter (array of data passed to each core)

mov rax,0
mov [rsp+40],rax            ; use default creation flags
mov rax,[ThreadCount]
mov [rsp+32],rax            ; ThreadID

call CreateThread

; Move the handle into ThreadHandles array (returned in rax)
mov rdi,ThreadHandles
mov rcx,[FirstByte]
mov [rdi+rcx],rax

mov rax,[FirstByte]
add rax,8
mov [FirstByte],rax

mov rax,[ThreadCount]
add rax,1
mov [ThreadCount],rax

mov rbx,4
cmp rax,rbx
jl label_0

; _____
; Wait

mov rcx,rax         ; number of handles
mov rdx,ThreadHandles       ; pointer to handles array
mov r8,1                ; wait for all threads to complete
mov r9,1000         ; milliseconds to wait

call WaitForMultipleObjects

; _____

;[ Code HERE to do cleanup if needed after the four threads finish ]

mov rsp,rbp
jmp label_900

; __________________
; The function for all threads to call

Test_Function:

; Populate registers
mov rdi,rcx
mov rax,[rdi]
mov r15,[rdi+24]
mov rax,[rdi+8] ; start byte
mov r13,[rdi+40]
mov r12,[rdi+48]
mov r10,[rdi+56]
xor r11,r11
xor r9,r9
pxor xmm15,xmm15
pxor xmm15,xmm14
pxor xmm15,xmm13

; Now test it - BUT the first thread does not write data
mov rcx,[rdi+8] ; start byte
mov rax,[rdi+16] ; stride
cvtsi2sd xmm0,rax
movsd [r15+rcx],xmm0
ret

1 Ответ

0 голосов
/ 06 марта 2019

Я решил эту проблему, и вот решение.Раймонд Чен упомянул об этом в комментариях выше, прежде чем убедить меня использовать язык более высокого уровня, но я не понимал его до сегодняшнего дня.Я публикую этот ответ, чтобы он был легко доступен и понят любому, кто столкнется с такой же проблемой на ассемблере (или на любом другом языке) в будущем, потому что комментарий Рэймонда (за который я только что проголосовал) теперь похоронен в других комментариях выше.

Массив ThreadInfo, переданный здесь в качестве четвертого параметра CreateThread (в r9 для Windows).Каждое ядро ​​должно иметь свою отдельную копию ThreadInfo.В моем приложении данные в ThreadInfo все те же, за исключением параметра StartByte (при rdi + 8).Вместо этого я создал отдельный массив ThreadInfo для каждого ядра (ThreadInfo1, 2, 3 и 4) и передал указатель на соответствующий массив ThreadInfo.

Я реализовал это в своем приложении как вызов следующей функции dup, но это можно реализовать и другими способами:

DupThreadInfo:
mov rdi,ThreadInfo2
mov rax,8
mov [rdi+8],rax
mov rax,[stride]
mov [rdi+16],rax    ; 8 x number of cores (32 in this example)
; Vars (these registers are populated on main entry)
mov [rdi+24],r15
mov [rdi+32],r14
mov [rdi+40],r13
mov [rdi+48],r12
mov [rdi+56],r10
; _____

mov rdi,ThreadInfo3
mov rax,0
mov [rdi],rax       ; length (number of vars into registers plus 3 elements)
mov rax,16
mov [rdi+8],rax
mov rax,[stride]
mov [rdi+16],rax    ; 8 x number of cores (32 in this example)
; Vars (these registers are populated on main entry)
mov [rdi+24],r15
mov [rdi+32],r14
mov [rdi+40],r13
mov [rdi+48],r12
mov [rdi+56],r10

mov rdi,ThreadInfo4
mov rax,0
mov [rdi],rax       ; length (number of vars into registers plus 3 elements)
mov rax,24
mov [rdi+8],rax
mov rax,[stride]
mov [rdi+16],rax    ; 8 x number of cores (32 in this example)
; Vars (these registers are populated on main entry)
mov [rdi+24],r15
mov [rdi+32],r14
mov [rdi+40],r13
mov [rdi+48],r12
mov [rdi+56],r10
ret

Поскольку все данные в массивах ThreadInfo одинаковы, кромевторой элемент, более эффективный способ сделать это - передать массив из 2 элементов, где первый элемент - это StartByte, а второй - указатель на статический массив ThreadInfo.Это особенно важно, когда мы работаем с более чем четырьмя ядрами, потому что раздел DupThreadInfo будет слишком длинным.Это решение позволит избежать звонка, но я еще не реализовал это.

...