Desenvolvimento

24 jan, 2014

Serialização Int32 no OCaml

Publicidade

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/