Hoje falarei um pouco sobre o problema de serializar um int32 no OCaml. Como estou trabalhando apenas com máquinas Intel, não estou interessado em portabilidade, e prefiro serialização “little-endian”. Isso deve ser fácil e natural.
A interface
val set32: string -> int -> int32 -> unit val get32: string -> int -> int32
O micro benchmark
Vamos armazenar um int32 em uma string, recuperá-lo, e checar se é o mesmo. Vamos fazer isso 1_000_000_000 vezes, ver quanto tempo leva e calcular a velocidade.
let benchmark n = let t0 = Unix.gettimeofday() in let s = String.create 4 in let limit = Int32.of_int n in let rec loop i32 = if i32 = limit then () else let () = set32 s 0 i32 in let j32 = get32 s 0 in assert (i32 = j32); loop (Int32.succ i32) in let () = loop 0l in let t1 = Unix.gettimeofday () in let d = t1 -. t0 in let speed = float n /. d in let megaspeed = speed /. 1000000.0 in Printf.printf "%i took %f => %fe6/s\n" n d megaspeed
Tentativa zero: implementação ingênua
Isso é bem objetivo: máscara, extrair o caractere, armazenar, mover e repetir. Recuperar o int32 da string é o oposto. Nenhuma grande ciência aqui.
Esse é um código simples e legível.
let set32_ocaml s pos (i:int32) = let (>:) = Int32.shift_right_logical in let (&:) = Int32.logand in let mask = Int32.of_int 0xff in let to_char v = Char.chr (Int32.to_int v) in let too_far = pos + 4 in let rec loop p i = if p = too_far then () else let vp = i &: mask in let cp = to_char vp in let () = s.[p] <- cp in loop (p+1) (i >: 8) in loop pos i let get32_ocaml s pos = let (<:) = Int32.shift_left in let (|:) = Int32.logor in let to_i32 c = Int32.of_int (Char.code c) in let rec loop acc p = if p < pos then acc else let cp = s.[p] in let vp = to_i32 cp in let acc' = (acc <: 8) |: vp in loop acc' (p-1) in loop 0l (pos + 3)
OCaml é uma linguagem de alto nível muito boa, mas essa troca de bits parece bagunçada e feia. De qualquer forma, vamos aos testes.
Estratégia | Velocidade
OCaml ingênuo | 16.0e6/s]
Uma olhada rápida sobre como isso é econômico
let get_byte32 i b = 255 land (Int32.to_int (Int32.shift_right i (8*b))) class trans = object(self) val ibyte = String.create 8 ... method writeI32 i = let gb = get_byte32 i in for i=0 to 3 do ibyte.[3-i] <- char_of_int (gb i) done; trans#write ibyte 0 4
Ok, isso usa a mesma estratégia; mas há um loop for por lá. A conversão é feita no buffer ibyte e então copiada de lá. Não é tão lindo, mas a cópia extra de 4 bytes também não deve ser custosa.
Tentativa 1: mas em C deve ser mais rápido
É uma frase que eu ouço muito, mas, nesse caso, realmente deve ser. Afinal, se você quer recuperar um int32 de uma string, tudo o que precisa fazer é jogar o char* para um int32* e de-referenciar o valor.
Vamos tentar isto:
external set32 : string -> int -> int32 -> unit = "zooph_set32" external get32 : string -> int -> int32 = "zooph_get32"
#include <stdint.h> #include <stdio.h> #include <caml/alloc.h> #include <caml/memory.h> #include <caml/mlvalues.h> value zooph_set32(value vs, value vpos, value vi){ CAMLparam3(vs, vpos, vi); char* buf = String_val(vs); int pos = Int_val(vpos); int32_t i = Int32_val(vi); char* buf_off = &buf[pos]; int32_t* casted = (int32_t*)buf_off; casted[0] = i; CAMLreturn (Val_unit); } value zooph_get32(value vs,value vpos){ CAMLparam2(vs,vpos); CAMLlocal1(result); char* buf = String_val(vs); int pos = Int_val(vpos); char* buf_off = &buf[pos]; int32_t* casted = (int32_t*)buf_off; int32_t i32 = casted[0]; result = caml_copy_int32(i32); CAMLreturn(result); }
Eu chamei minha unidade de compilação de zooph.c, uma onomatopeia que presta tributo à velocidade que eu acho que isso terá. Não há loop, e a máquina possui a capacidade de fazer a transformação em um passo. Então deve ser, pelo menos, 4 vezes mais rápido. Vamos ao teste.
Estratégia Velocidade
OCaml ingênua 16.0e6
C via FFI 32.3e6
Hum… já está mais rápido, mas também foi um pouco decepcionante. Então, o que deu errado?
Uma olhada rápida no código assembly revela muita coisa:
zooph_set32: .LFB34: .cfi_startproc movl 8(%rdx), %eax sarq %rsi movslq %esi, %rsi movl %eax, (%rdi,%rsi) movl $1, %eax ret .cfi_endproc .LFE34: .size zooph_set32, .-zooph_set32 .p2align 4,,15 .globl zooph_get32 .type zooph_get32, @function zooph_get32: .LFB35: .cfi_startproc pushq %rbx .cfi_def_cfa_offset 16 .cfi_offset 3, -16 movq %rsi, %rdx sarq %rdx subq $160, %rsp .cfi_def_cfa_offset 176 movslq %edx, %rdx movq caml_local_roots(%rip), %rbx leaq 8(%rsp), %rcx movq %rdi, 8(%rsp) movl (%rdi,%rdx), %edi movq %rsi, (%rsp) movq $1, 32(%rsp) movq %rcx, 40(%rsp) leaq (%rsp), %rcx movq %rbx, 16(%rsp) movq $2, 24(%rsp) movq $0, 152(%rsp) movq %rcx, 48(%rsp) leaq 16(%rsp), %rcx movq $1, 96(%rsp) movq $1, 88(%rsp) movq %rcx, 80(%rsp) leaq 80(%rsp), %rcx movq %rcx, caml_local_roots(%rip) leaq 152(%rsp), %rcx movq %rcx, 104(%rsp) call caml_copy_int32 movq %rbx, caml_local_roots(%rip) addq $160, %rsp .cfi_def_cfa_offset 16 popq %rbx .cfi_def_cfa_offset 8 ret .cfi_endproc
Enquanto o zooph_se32 parece estar em ordem, sua contra parte está bem bagunçada. Em uma inspeção mais de perto, nem mesmo o lado do set32 está otimizado. O FFI do OCaml permite uma interação suave (ao menos quando comparado co o jni) com o código nativo de outras linguagens e também instala uma divisão concreta onde não é possível ter código inline (não com o OCaml).
Vejamos como o código do teste é chamado aqui.
.L177: movq %rbx, 8(%rsp) movq %rax, 0(%rsp) movq $1, %rsi movq 16(%rbx), %rdi movq %rax, %rdx movq zooph_set32@GOTPCREL(%rip), %rax call caml_c_call@PLT .L179: movq caml_young_ptr@GOTPCREL(%rip), %r11 movq (%r11), %r15 movq $1, %rsi movq 8(%rsp), %rax movq 16(%rax), %rdi movq zooph_get32@GOTPCREL(%rip), %rax call caml_c_call@PLT .L180: movq caml_young_ptr@GOTPCREL(%rip), %r11 movq (%r11), %r15 movslq 8(%rax), %rax movq 0(%rsp), %rdi movslq 8(%rdi), %rbx cmpq %rax, %rbx je .L176
Você vê coisas sendo enviadas para a pilha antes da chamada. Para uma alta velocidade, você nem quer uma chamada. Para chegarmos lá, precisamos traduzir o benchmark para C também. Não vou me preocupar com isso, porque eu tenho outro truque pronto.
Tentativa 2: primitivos do OCaml 4.01
OCaml 4.01 foi lançado recentemente e há uma pequena entrada nas notas de lançamento.
PR#5771: Add primitives for reading 2, 4, 8 bytes in strings and bigarrays – (Pierre Chambart)
Entretanto, por alguma razão, eles não estão realmente expostos e tive que procurar mais fundo para encontrá-los. Usá-los, no entanto, é bem trivial.
external get32_prim : string -> int -> int32 = "%caml_string_get32" external set32_prim : string -> int -> int32 -> unit = "%caml_string_set32"
E é só isso. Basicamente, você diz que sabe que o compilador sabe como fazer isso e que, a partir de agora, quer que ele faça isso.
Vamos ao teste.
Estratégia Velocidade
OCaml ingênuo 16,0e6
C via FFI 32.3e6
OCaml com primitivos 139e6
Uau!
Palavras finais
Coloquei o código para isso no GitHub. De qualquer forma, também precisamos (de)serializar valores int64. Determinar a velocidade que sobra como um exercício para o leitor (dica: é ainda melhor).
Acho que algumas pessoas sentirão a necessidade de também aplicar isso a seu código de serialização.
Divirta-se!
***
Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em http://blog.incubaid.com/2013/10/04/int32-serialization-in-ocaml/