web-dev-qa-db-fra.com

Un moyen rapide de remplacer des éléments dans un tableau - C

Supposons que nous ayons un tableau d'ints comme ceci:

const int size = 100000;
int array[size];
//set some items to 0 and other items to 1

Je voudrais remplacer tous les éléments qui ont une valeur de 1 par une autre valeur, par exemple 123456. Cela peut être mis en œuvre de manière triviale avec:

for(int i = 0; i < size ; i++){
    if(array[i] != 0) 
        array[i] = 123456;
}

Par curiosité, existe-t-il un moyen plus rapide de le faire, par une sorte de ruse x86, ou est-ce le meilleur code pour le processeur?

31
Axarydax

Pour votre cas spécifique où vous avez initialement 0 et 1, ce qui suit pourrait être plus rapide. Vous devrez le comparer. Cependant, vous ne pouvez probablement pas faire beaucoup mieux avec du C ordinaire; vous devrez peut-être plonger dans Assembly si vous voulez profiter de la "supercherie x86" qui peut exister.

for(int i = 0; i < size ; i++){
  array[i] *= 123456;
}

MODIFIER:

Code de référence:

#include <time.h>
#include <stdlib.h>
#include <stdio.h>

size_t diff(struct timespec *start, struct timespec *end)
{
  return (end->tv_sec - start->tv_sec)*1000000000 + end->tv_nsec - start->tv_nsec;
}

int main(void)
{
  const size_t size = 1000000;
  int array[size];

  for(size_t i=0; i<size; ++i) {
    array[i] = Rand() & 1;
  }

  struct timespec start, stop;

  clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start);
  for(size_t i=0; i<size; ++i) {
    array[i] *= 123456;
    //if(array[i]) array[i] = 123456;
  }
  clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &stop);

  printf("size: %zu\t nsec: %09zu\n", size, diff(&start, &stop));
}

mes résultats:

Ordinateur: quad core AMD Phenom @ 2,5 GHz, Linux, GCC 4.7, compilé avec

$ gcc arr.c -std=gnu99 -lrt -O3 -march=native
  • if version: ~ 5-10 ms
  • *= version: ~ 1,3 ms
47
Nicu Stiurca

Pour un petit tableau tel que le vôtre, il est inutile d'essayer de trouver un autre algorithme, et si les valeurs ne sont pas dans un modèle spécifique, une simple boucle est le seul moyen de le faire de toute façon.

Cependant, si vous avez un très grand tableau (nous parlons de plusieurs millions d'entrées), vous pouvez diviser le travail en threads. Chaque thread séparé gère une plus petite partie de l'ensemble de données.

15

Vous pouvez également comparer cela:

for(int i = 0; i < size ; i++){
  array[i] = (~(array[i]-1) & 123456);
}

Je le fais passer par le même benchmark que SchighSchagh, avec peu ou pas de différence sur ma configuration. Cela peut toutefois différer du vôtre.

EDIT: Arrêtez les presses!

Je viens de me rappeler que x86 peut "débrancher" les opérateurs ternaires si les arguments entre ":" sont des constantes. Considérez le code suivant:

for(size_t i=0; i<size; ++i) {
    array[i] = array[i] ? 123456 : 0;
}

Ressemble presque à votre code d'origine, n'est-ce pas? Eh bien, le démontage montre qu'il a été compilé sans aucune branche:

  for(size_t i=0; i<size; ++i) {
00E3104C  xor         eax,eax  
00E3104E  mov         edi,edi  
        array[i] = array[i] ? 123456 : 0;
00E31050  mov         edx,dword ptr [esi+eax*4]  
00E31053  neg         edx  
00E31055  sbb         edx,edx  
00E31057  and         edx,1E240h  
00E3105D  mov         dword ptr [esi+eax*4],edx  
00E31060  inc         eax  
00E31061  cmp         eax,5F5E100h  
00E31066  jb          wmain+50h (0E31050h)  
    }

En termes de performances, il semble à peu près égal ou légèrement supérieur à ma solution originale et SchighSchagh. Il est cependant plus lisible et plus flexible. Par exemple, il peut fonctionner avec le tableau [i] ayant des valeurs différentes de 0 et 1.

Conclusion, référence ET jetez un œil au démontage.

13
gwiazdorrr

Le tableau est suffisamment petit pour tenir dans le cache, il devrait donc être intéressant d'utiliser SIMD: (non testé)

  mov ecx, size
  lea esi, [array + ecx * 4]
  neg ecx
  pxor xmm0, xmm0
  movdqa xmm1, [_vec4_123456]  ; value of { 123456, 123456, 123456, 123456 }
_replaceloop:
  movdqa xmm2, [esi + ecx * 4] ; assumes the array is 16 aligned, make that true
  add ecx, 4
  pcmpeqd xmm2, xmm0
  pandn xmm2, xmm1
  movdqa [esi + ecx * 4 - 16], xmm2
  jnz _replaceloop

Dérouler par 2 pourrait aider.

Si vous avez SSE4.1, vous pouvez utiliser l'astuce de multiplication de SchighSchagh avec pmulld.

7
harold

Voici du code Win32 pour profiler différentes versions de l'algorithme (compilé à l'aide de VS2010 Express en utilisant la version par défaut): -

#include <windows.h>
#include <stdlib.h>
#include <stdio.h>

const size_t
  size = 0x1D4C00;

_declspec(align(16)) int
  g_array [size];

_declspec(align(16)) int
  _vec4_123456 [] = { 123456, 123456, 123456, 123456 };

void Test (void (*fn) (size_t, int *), char *test)
{
  printf ("Executing test: %s\t", test);

  for(size_t i=0; i<size; ++i) {
    g_array[i] = Rand() & 1;
  }

  LARGE_INTEGER
    start,
    end;

  QueryPerformanceCounter (&start);

  fn (size, g_array);

  QueryPerformanceCounter (&end);

  printf("size: %u\t count: %09u\n", size, (int) (end.QuadPart - start.QuadPart));
}

void Test1 (size_t size, int *array)
{
  for(size_t i=0; i<size; ++i) {
    array[i] *= 123456;
  }
}

void Test2 (size_t size, int *array)
{
  for(size_t i=0; i<size; ++i) {
    if(array[i]) array[i] = 123456;
  }
}

void Test3 (size_t array_size, int *array)
{
  __asm
  {
    mov edi,array
    mov ecx, array_size 
    lea esi, [edi + ecx * 4]
    neg ecx
    pxor xmm0, xmm0
    movdqa xmm1, [_vec4_123456]  ; value of { 123456, 123456, 123456, 123456 }
_replaceloop:
    movdqa xmm2, [esi + ecx * 4] ; assumes the array is 16 aligned, make that true
    add ecx, 4
    pcmpeqd xmm2, xmm0
    pandn xmm2, xmm1
    movdqa [esi + ecx * 4 - 16], xmm2
    jnz _replaceloop
  }
}

void Test4 (size_t array_size, int *array)
{
  array_size = array_size * 8 / 12;

  __asm
  {
        mov edi,array
        mov ecx,array_size
        lea esi,[edi+ecx*4]
                                      lea edi,[edi+ecx*4]
        neg ecx
                                      mov edx,[_vec4_123456]
        pxor xmm0,xmm0
        movdqa xmm1,[_vec4_123456]
replaceloop:
        movdqa xmm2,[esi+ecx*4]
                                      mov eax,[edi]
                                      mov ebx,[edi+4]
        movdqa xmm3,[esi+ecx*4+16]
                                      add edi,16
        add ecx,9
                                      imul eax,edx    
        pcmpeqd xmm2,xmm0
                                      imul ebx,edx
        pcmpeqd xmm3,xmm0
                                      mov [edi-16],eax
                                      mov [edi-12],ebx
        pandn xmm2,xmm1
                                      mov eax,[edi-8]
                                      mov ebx,[edi-4]
        pandn xmm3,xmm1
                                      imul eax,edx    
        movdqa [esi+ecx*4-36],xmm2
                                      imul ebx,edx
        movdqa [esi+ecx*4-20],xmm3
                                      mov [edi-8],eax
                                      mov [edi-4],ebx
        loop replaceloop
  }
}

int main()
{
    Test (Test1, "Test1 - mul");
    Test (Test2, "Test2 - branch");
    Test (Test3, "Test3 - simd");
    Test (Test4, "Test4 - simdv2");
}

C'est pour les tests: C en utilisant une if()..., C en utilisant un multiplier, la version simd de harold et ma version simd.

En l'exécutant plusieurs fois (rappelez-vous, lors du profilage, vous devez faire la moyenne des résultats sur plusieurs exécutions), il y a peu de différence entre toutes les versions, à l'exception de la branche, qui est considérablement plus lente.

Ce n'est pas très surprenant car l'algortihm fait très peu de travail pour chaque élément de la mémoire. Cela signifie que le véritable facteur limitant est la bande passante entre le processeur et la mémoire, le processeur attend constamment que la mémoire rattrape son retard, même avec le processeur aidant à la prélecture des données (la détection et la prélecture des données de l'ia32 linéairement).

3
Skizz

Vous pouvez utiliser un autre tableau ou une autre structure de données pour garder une trace des index des éléments que vous définissez sur un, puis visiter uniquement ces éléments. Cela fonctionnera mieux s'il n'y a que quelques éléments qui sont définis sur un

2
Sven

Cela pourrait s'avérer plus rapide.

for(int i = 0; i < size ; i++){
  array[i] = ((123456 << array[i]) - 123456);
}

EDIT: Opération au niveau du bit changée en décalage à gauche.

2
Abhinav

une autre façon d'accélérer l'affectation de la baie, vous pouvez utiliser l'assemblage c en ligne. Comme ci-dessous,

#include<stdio.h>
#include<string.h>
#include<stdlib.h>

const int size = 100000; 
void main(void) {
  int array[size];
  int value = 1000;

  __asm__ __volatile__("cld\n\t"
          "rep\n\t"
          "stosl\n\t"
          :
          :"c"(size*4), "a"(value), "D"(array)
          :
         );

  printf("Array[0] : %d \n", array[0]);
}

Cela devrait être la vitesse lorsque nous comparons au programme c simple pour attribuer les valeurs du tableau. Et aussi l'instruction stosl prend 4 cycles d'horloge.

0
Mohanraj