2016-07-15 435 views
9

Inizialmente esaminando l'effetto della direttiva #pragma omp simd, mi sono imbattuto in un comportamento che non riesco a spiegare, relativo alla vettorizzazione di un ciclo for semplice. Il seguente esempio di codice può essere testato su questo fantastico compiler explorer, a condizione che venga applicata la direttiva -O3 e che siamo sull'architettura x86.Comportamento GCC intransigente rispetto alla vettorizzazione e dimensione del loop

Qualcuno potrebbe spiegarmi la logica dietro le seguenti osservazioni?

#include <stdint.h> 

void test(uint8_t* out, uint8_t const* in, uint32_t length) 
{ 
    unsigned const l1 = (length * 32)/32; // This is vectorized 
    unsigned const l2 = (length/32)*32; // This is not vectorized 

    unsigned const l3 = (length << 5)>>5; // This is vectorized 
    unsigned const l4 = (length >> 5)<<5; // This is not vectorized 

    unsigned const l5 = length -length%32; // This is not vectorized 
    unsigned const l6 = length & ~(32 -1); // This is not vectorized 

    for (unsigned i = 0; i<l1 /*pick your choice*/; ++i) 
    { 
     out[i] = in[i*2]; 
    } 
} 

cosa mi lascia perplesso è che sia L1 ed L3 generare il codice vettorializzare nonostante non beeing garantito per essere multipli di 32. Tutte le altre lunghezze fanno non produrre codice vettorizzati, ma devono essere multipli di 32 C'è una ragione dietro questo?

Come parte, l'uso della direttiva #pragma omp simd in realtà non cambia nulla.

Edit: Dopo ulteriori indagini, la differenza di comportamento scompare quando il tipo di indice è size_t (e nessuna manipolazione di confine è ancora necessario), il che significa che questo genera codice vettorizzati:

#include <stdint.h> 
#include <string> 

void test(uint8_t* out, uint8_t const* in, size_t length) 
{ 
    for (size_t i = 0; i<length; ++i) 
    { 
     out[i] = in[i*2]; 
    } 
} 

Se qualcuno sa perché la il ciclo vettoriale è così dipendente dal tipo di indice, quindi sarei curioso di saperne di più!

Edit2, grazie a Mark Lakata, O3 è effettivamente necessario

+0

In quello che potrebbe essere visto come un'estensione di questa domanda, lo stesso identico comportamento è visibile con Clang, quindi suppongo che ci sia un po 'di logica in esso. –

+1

Sembra che il compilatore teme che l'indice possa avvolgere e rinuncia per questo :-( –

+0

Mi è stata spiegata la dipendenza dal tipo, legata al rischio di overflow (che impedisce la vettorizzazione). È consentito un overflow senza segno , mentre un overflow firmato non lo è, il che spiega quest'ultimo punto: l'utilizzo di un segno non firmato e il primo (eliminando efficacemente il rischio di overflow) consente la vettorizzazione, GCC è super intelligente: https://godbolt.org/g/SsVZ2r –

risposta

4

Il problema risulta conversione da unsigned al size_t nell'indice dell'array: in[i*2];

Se si utilizza l1 o l3 allora il calcolo di i*2 si adatta sempre al tipo size_t. Ciò significa che il tipo unsigned si comporta praticamente come se fosse size_t.

Tuttavia, quando si utilizzano le altre opzioni, il risultato del calcolo i*2 potrebbe non rientrare in size_t poiché il valore potrebbe essere disposto e la conversione deve essere eseguita.

se si prende il primo esempio, non scegliere le opzioni L1 o L3, e fare il cast:

out[i] = in[(size_t)i*2]; 

il compilatore ottimizza, se lanci l'intera espressione:

out[i] = in[(size_t)(i*2)]; 

doesn 't.


Lo standard realtà non specificare che il tipo nell'indice deve essere size_t, ma è un passo logico dalla prospettiva compilatore.

+0

Non sono sicuro che gli indici vengano convertiti in 'size_t' quando si dereferenzia un puntatore, sebbene la possibilità che l'overflow di cui parli sia ancora rilevante – SirGuy

+0

@GuyGreer Secondo lo Standard, non lo sono, vedere l'aggiornamento. – 2501

+1

Ancora non sono d'accordo con il problema di essere in qualsiasi tipo di conversione da 'unsigned' a' size_t' (che su una macchina a 32 bit è un no-op). La tua risposta ha molto più senso per me dichiarata in termini di trattare con wraparound e quando il compilatore può provare a se stesso che non accadrà. – SirGuy

1

Credo che si confonda l'ottimizzazione con la vettorizzazione. Ho usato il tuo compiler explorer e impostato -O2 per x86, e nessuno degli esempi è "vettorizzato".

Ecco l1

test(unsigned char*, unsigned char const*, unsigned int): 
     xorl %eax, %eax 
     andl $134217727, %edx 
     je  .L1 
.L5: 
     movzbl (%rsi,%rax,2), %ecx 
     movb %cl, (%rdi,%rax) 
     addq $1, %rax 
     cmpl %eax, %edx 
     ja  .L5 
.L1: 
     rep ret 

Ecco l2

test(unsigned char*, unsigned char const*, unsigned int): 
     andl $-32, %edx 
     je  .L1 
     leal -1(%rdx), %eax 
     leaq 1(%rdi,%rax), %rcx 
     xorl %eax, %eax 
.L4: 
     movl %eax, %edx 
     addq $1, %rdi 
     addl $2, %eax 
     movzbl (%rsi,%rdx), %edx 
     movb %dl, -1(%rdi) 
     cmpq %rcx, %rdi 
     jne  .L4 
.L1: 
     rep ret 

Questo non è sorprendente, perché quello che stai facendo è essenzialmente un "raccogliere" operazione di caricamento, dove gli indici di carico non sono come gli indici del negozio. Non c'è supporto in x86 per gather/scatter. Viene introdotto solo in AVX2 e AVX512 e non è selezionato.

Il codice leggermente più lungo riguarda i problemi firmati/non firmati, ma non è in corso alcuna vettorizzazione.

+0

Grazie per aver chiarito la vettorizzazione. Puoi approfondire il tema firmato/non firmato. La sorgente C non contiene tipi firmati, quindi perché dovrebbero essere utilizzati in assembly? – 2501

+0

beh, non lo so, ma suppongo che abbia a che fare con limitazioni del carico indiretto 'movzbl (% rsi,% rax, 2),% ecx' e che% rax debba essere inferiore a 32 bit altrimenti la scala di 2 andrà in overflow. Ma non sono riuscito a trovare rapidamente la risposta su google ... –

+0

fwiw, c'è un valore firmato nel tuo codice. La costante 2 è firmata ... ma non importa in questa discussione. –