strlen_sse42() usando função “intrínseca”.

Pode parecer que a instrução PCMPISTRI seja meio complicada de usar em C, já que ela oferece dois resultados diferentes: ECX, contendo um índice de acordo com a comparação, e os flags. Mas, felizmente, o valor de ECX será 16 se InRes2 estiver totalmente zerado! Assim, a função anterior, escrita em assembly, pode ser reescrita em C assim:

/* test.c

  Compilar com:
    gcc -Ofast -msse4.2 -o test test.c
*/
#include <stddef.h>
#include <x86intrin.h>

size_t strlen_sse42_c(const char *s)
{
  unsigned int index;
  size_t result;
  static const char ranges[16] = { 1, 255 };

  result = 0;
  do
  {
    index = _mm_cmpistri(*(__m128i *)ranges, 
                         *(__m128i *)s,
                         _SIDD_UBYTE_OPS         | 
                         _SIDD_CMP_RANGES        | 
                         _SIDD_NEGATIVE_POLARITY |
                         _SIDD_LEAST_SIGNIFICANT);

    result += index;
    s += sizeof(__m128i);
  } while (count == 16);

  return result;
}

O código final ficará semelhante, mas menos performático, ao anterior:

bits 64

section .rodata

  align 16
_ranges:  db 1, 255
          times 14 db 0

section .text

global strlen_sse42:
  align 16
strlen_sse42:
  movdqa xmm0,[_ranges]
  xor    eax,eax

.loop:
  pcmpistri xmm0, [rdi], 0x0_01_01_0_0  
  mov    edx,ecx
  add    rdi,16
  add    rax,rdx
  cmp    ecx,16
  jz     .loop
  
  ret

Algumas diferenças óbvias: a instrução MOVDQA é mais rápida que MOVDQU e exige que o array _ranges esteja alinhado. Eu deveria ter previsto isso no código em assembly no artigo anterior… O compilador escolheu fazer DUAS comparações, como instruído. Como não temos como verificar o flag ZF à partir da função intrínseca _mm_cmpistri, só nos restava comparar o valor retornado com 16.

Agora… é evidente que PCMPISTRI só está disponível se seu processador suportar SSE 4.2. Um método bem simples de usar essa função OU a função padrão do compilador é este:

#include <stddef.h>
#include <string.h>
#include <x86intrin.h>

// Daqui para frente, strlen será chamada por esse ponteiro!
size_t (*__strlen)(const char *);

static size_t strlen_sse42_c(const char *s)
{ ... }

// Esse atributo faz com que a função seja executada
// ANTES de main(). É interessante ter apenas uma dessas
// funções em seu programa, embora o atributo permita definir
// a ordem de execução...
static __attribute__((constructor)) void ctor(void)
{
  if (__builtin_cpu_supports("sse4.2"))
    __strlen = strlen_sse42_c;
  else
    __strlen = strlen;
}

As chamadas a __stlen, evidentemente, serão sempre indiretas, mas assim você garante a compatibilidade entre processadores ao usar a rotina. Além do mais, a quase totalidade das funções da libc são chamadas de forma indireta, já que localizam-se em libc6.so.

Anúncios