F # Breakable Array Итерация с проверкой границ Elided - PullRequest
0 голосов
/ 10 ноября 2018

Мне интересно попробовать F # в высокопроизводительном приложении. Я не хочу, чтобы границы большого массива проверялись во время итерации, и отсутствие операторов break / return вызывает беспокойство.

Это надуманный пример, который сломается при поиске значения, но может ли кто-нибудь сказать мне, если проверка границ также разрешена?

let innerExists (item: Char) (items: Char array): bool = 
    let mutable state = false
    let mutable i = 0
    while not state && i < items.Length do
        state <- item = items.[i]
        i <- i + 1

    state

let exists (input: Char array)(illegalChars: Char array): bool = 
    let mutable state = false
    let mutable i = 0
    while not state && i < input.Length do
        state <- innerExists input.[i] illegalChars
        i <- i + 1

    state

exists [|'A'..'z'|] [|'.';',';';'|]

Вот соответствующая разборка:

    while not state && i < input.Length do
000007FE6EB4237A  cmp         dword ptr [rbp-14h],0  
000007FE6EB4237E  jne         000007FE6EB42383  
000007FE6EB42380  nop  
000007FE6EB42381  jmp         000007FE6EB42386  
000007FE6EB42383  nop  
000007FE6EB42384  jmp         000007FE6EB423A9  
000007FE6EB42386  nop  
000007FE6EB42387  mov         r8d,dword ptr [rbp-18h]  
000007FE6EB4238B  mov         rdx,qword ptr [rbp+18h]  
000007FE6EB4238F  cmp         r8d,dword ptr [rdx+8]  
000007FE6EB42393  setl        r8b  
000007FE6EB42397  movzx       r8d,r8b  
000007FE6EB4239B  mov         dword ptr [rbp-24h],r8d  
000007FE6EB4239F  mov         r8d,dword ptr [rbp-24h]  
000007FE6EB423A3  mov         dword ptr [rbp-1Ch],r8d  
000007FE6EB423A7  jmp         000007FE6EB423B1  
000007FE6EB423A9  nop  
000007FE6EB423AA  xor         r8d,r8d  
000007FE6EB423AD  mov         dword ptr [rbp-1Ch],r8d  
000007FE6EB423B1  cmp         dword ptr [rbp-1Ch],0  
000007FE6EB423B5  je          000007FE6EB42409  
            state <- innerExists input.[i] illegalChars
000007FE6EB423B7  mov         r8d,dword ptr [rbp-18h]  
000007FE6EB423BB  mov         rdx,qword ptr [rbp+18h]  
000007FE6EB423BF  cmp         r8,qword ptr [rdx+8]  
000007FE6EB423C3  jb          000007FE6EB423CA  
000007FE6EB423C5  call        000007FECD796850  
000007FE6EB423CA  lea         rdx,[rdx+r8*2+10h]  
000007FE6EB423CF  movzx       r8d,word ptr [rdx]  
000007FE6EB423D3  mov         rdx,qword ptr [rbp+10h]  
000007FE6EB423D7  mov         rdx,qword ptr [rdx+8]  
000007FE6EB423DB  mov         r9,qword ptr [rbp+20h]  
000007FE6EB423DF  mov         rcx,7FE6EEE0640h  
000007FE6EB423E9  call        000007FE6EB41E40  
000007FE6EB423EE  mov         dword ptr [rbp-20h],eax  
000007FE6EB423F1  mov         eax,dword ptr [rbp-20h]  
000007FE6EB423F4  movzx       eax,al  
000007FE6EB423F7  mov         dword ptr [rbp-14h],eax  
            i <- i + 1
000007FE6EB423FA  mov         eax,dword ptr [rbp-18h]  

Ответы [ 3 ]

0 голосов
/ 10 ноября 2018

Проверка привязки исключена компилятором JIT, поэтому для F # она работает так же, как и для C #. Вы можете ожидать исключения для кода, как в вашем примере, а также для

for i = 0 to data.Lenght - 1 do
    ...

, а также для хвостовых рекурсивных функций, которые компилируются в циклы.

Встроенные Array.contains и Array.exists (исходный код) написаны так, что JIT-компилятор может исключить проверки границ.

0 голосов
/ 11 ноября 2018

Другие указали на использование существующей функции FSharp.Core для достижения того же результата, но я думаю, что OP спрашивает, исключены ли в циклах, таких как проверка границ массивов (как это проверяется в условии цикла).

Для простого кода, подобного указанному выше, джиттер должен быть в состоянии исключить проверки. Чтобы убедиться в этом, правильно проверить код сборки, но важно не запускать с подключенным отладчиком VS, так как джиттер не оптимизирует код. Причина, по которой невозможно отобразить правильные значения в отладчике.

Сначала давайте посмотрим на exists оптимизированный x64:

; not state?
00007ff9`1cd37551 85c0            test    eax,eax
; if state is true then exit the loop
00007ff9`1cd37553 7521            jne     00007ff9`1cd37576
; i < input.Length?
00007ff9`1cd37555 395e08          cmp     dword ptr [rsi+8],ebx
; Seems overly complex but perhaps this is as good as it gets?
00007ff9`1cd37558 0f9fc1          setg    cl
00007ff9`1cd3755b 0fb6c9          movzx   ecx,cl
00007ff9`1cd3755e 85c9            test    ecx,ecx
; if we have reached end of the array then exit
00007ff9`1cd37560 7414            je      00007ff9`1cd37576
; mov i in ebx to rcx, unnecessary but moves like these are very cheap
00007ff9`1cd37562 4863cb          movsxd  rcx,ebx
; input.[i] (note we don't check the boundary again)
00007ff9`1cd37565 0fb74c4e10      movzx   ecx,word ptr [rsi+rcx*2+10h]
; move illegalChars pointer to rdx
00007ff9`1cd3756a 488bd7          mov     rdx,rdi
; call innerExists
00007ff9`1cd3756d e8ee9affff      call    00007ff9`1cd31060
; i <- i + 1
00007ff9`1cd37572 ffc3            inc     ebx
; Jump top of loop
00007ff9`1cd37574 ebdb            jmp     00007ff9`1cd37551
; We are done!
00007ff9`1cd37576

Таким образом, код выглядит слишком сложным для того, каким он должен быть, но кажется, что он проверяет состояние массива только один раз.

Теперь давайте посмотрим на innerExists оптимизированный x64:

# let mutable state = false
00007ff9`1cd375a0 33c0            xor     eax,eax
# let mutable i = 0
00007ff9`1cd375a2 4533c0          xor     r8d,r8d
; not state?
00007ff9`1cd375a5 85c0            test    eax,eax
; if state is true then exit the loop
00007ff9`1cd375a7 752b            jne     00007ff9`1cd375d4
; i < items.Length
00007ff9`1cd375a9 44394208        cmp     dword ptr [rdx+8],r8d
; Seems overly complex but perhaps this is as good as it gets?
00007ff9`1cd375ad 410f9fc1        setg    r9b
00007ff9`1cd375b1 450fb6c9        movzx   r9d,r9b
00007ff9`1cd375b5 4585c9          test    r9d,r9d
; if we have reached end of the array then exit
00007ff9`1cd375b8 741a            je      00007ff9`1cd375d4
; mov i in r8d to rax, unnecessary but moves like these are very cheap
00007ff9`1cd375ba 4963c0          movsxd  rax,r8d
; items.[i] (note we don't check the boundary again)
00007ff9`1cd375bd 0fb7444210      movzx   eax,word ptr [rdx+rax*2+10h]
; mov item in cx to r9d, unnecessary but moves like these are very cheap
00007ff9`1cd375c2 440fb7c9        movzx   r9d,cx
; item = items.[i]?
00007ff9`1cd375c6 413bc1          cmp     eax,r9d
00007ff9`1cd375c9 0f94c0          sete    al
; state <- ?
00007ff9`1cd375cc 0fb6c0          movzx   eax,al
; i <- i + 1
00007ff9`1cd375cf 41ffc0          inc     r8d
; Jump top of loop
00007ff9`1cd375d2 ebd1            jmp     00007ff9`1cd375a5
; We are done!
00007ff9`1cd375d4 c3              ret

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

Итак, наконец, похоже, что джиттер устраняет проверки границ массива, потому что он может доказать, что он уже был успешно проверен в условии цикла, что, как я полагаю, и было вопросом ОП.

Код x64 выглядит не так чисто, как мог бы, но из моих экспериментов очищенный код x64 работает не намного лучше, я подозреваю, что производители ЦП оптимизируют ЦП для создания дрянного кода.

Интересным упражнением было бы написать код эквивалентной программы на C ++ и запустить ее с помощью https://godbolt.org/,, выбрать x86-64 gcc (trunk) (gcc, кажется, лучше всего подходит прямо сейчас) и указать параметры -O3 -march=native и увидеть результат код x64.

Обновление

Код, переписанный в https://godbolt.org/, чтобы позволить нам увидеть код сборки, сгенерированный компилятором c ++:

template<int N>
bool innerExists(char item, char const (&items)[N]) {
    for (auto i = 0; i < N; ++i) {
        if (item == items[i]) return true;
    }
    return false;
}

template<int N1, int N2>
bool exists(char const (&input)[N1], char const (&illegalCharacters)[N2]) {
    for (auto i = 0; i < N1; ++i) {
        if (innerExists(input[i], illegalCharacters)) return true;
    }
    return false;
}

char const separators[] = { '.', ',', ';' };
char const str[58] = {  };

bool test() {
  return exists(str, separators);
}

x86-64 gcc (trunk) с параметрами -O3 -march=native генерируется следующий код

; Load the string to test into edx
mov edx, OFFSET FLAT:str+1
.L2:
; Have we reached the end?
cmp rdx, OFFSET FLAT:str+58
; If yes, then jump to the end
je .L7
; Load a character
movzx ecx, BYTE PTR [rdx]
; Comparing the 3 separators are encoded in the assembler
;  because the compiler detected the array is always the same
mov eax, ecx
and eax, -3
cmp al, 44
sete al
cmp cl, 59
sete cl
; increase outer i
inc rdx
; Did we find a match?
or al, cl
; If no then loop to .L2
je .L2
; We are done!
ret
.L7:
; No match found, clear result
xor eax, eax
; We are done!
ret

Выглядит довольно хорошо, но в коде выше я упустил использование AVX для проверки нескольких символов одновременно.

0 голосов
/ 10 ноября 2018

Что не так с Array.contains и Array.exists функции?

let exists input illegalChars = 
    input |> Array.exists (fun c -> illegalChars |> Array.contains c)
...