it-swarm.it

Perché il bit-shift sinistro, "<<", per gli interi a 32 bit funziona come previsto se usato più di 32 volte?

Quando scrivo il seguente programma e utilizzo il GNU, l'output è 1 che ritengo sia dovuto all'operazione di rotazione eseguita dal compilatore.

#include <iostream>

int main()
{
    int a = 1;
    std::cout << (a << 32) << std::endl;

    return 0;
}

Ma logicamente, poiché si dice che i bit vengano persi se traboccano la larghezza dei bit, l'output dovrebbe essere 0. Cosa sta succedendo?

Il codice è su ideone, http://ideone.com/VPTwj .

58
AnkitSablok

Ciò è causato da una combinazione di un comportamento indefinito in C e dal fatto che il codice generato per i processori IA-32 ha una maschera a 5 bit applicata sul conteggio dei turni. Ciò significa che sui processori IA-32, l'intervallo di un conteggio dei turni è solo 0-31 1

Da Il linguaggio di programmazione C 2

Il risultato non è definito se l'operando di destra è negativo, o maggiore o uguale al numero di bit nel tipo dell'espressione sinistra.

Da Manuale dello sviluppatore del software Intel Architecture IA-32 3

8086 non maschera il conteggio dei turni. Tuttavia, tutti gli altri processori IA-32 (a partire dal processore Intel 286) mascherano il conteggio dei turni su 5 bit, risultando in un conteggio massimo di 31. Questo mascheramento viene eseguito in tutte le modalità operative (inclusa la modalità virtuale-8086) per ridurre il tempo massimo di esecuzione delle istruzioni.



1 http://codeyarns.com/2004/12/20/c-shift-operator-mayhem/

2 A7.8 Operatori a turni, Appendice A. Manuale di riferimento, Il linguaggio di programmazione C

3 SAL/SAR/SHL/SHR - Shift, Capitolo 4. Riferimento set istruzioni, IA-32 Manuale dello sviluppatore del software Intel Architecture

41
Ashwin Nanjappa

In C++, shift è ben definito solo se si sposta un valore di meno passaggi rispetto alla dimensione del tipo. Se int è 32 bit, allora solo 0 a, compreso, 31 passi è ben definito.

Allora, perché?

Se dai un'occhiata all'hardware sottostante che esegue lo shift, se deve solo guardare i cinque bit inferiori di un valore (nel caso a 32 bit), può essere implementato usando meno porte logiche rispetto a se deve ispezionare ogni bit del valore.

Rispondi alla domanda nel commento

C e C++ sono progettati per funzionare il più velocemente possibile, su qualsiasi hardware disponibile. Oggi, il codice generato è semplicemente un'istruzione '' shift '', indipendentemente dal modo in cui l'hardware sottostante gestisce i valori al di fuori dell'intervallo specificato. Se le lingue avrebbero specificato come dovrebbe comportarsi il turno, il generato potrebbe verificare che il conteggio dei turni sia compreso nell'intervallo prima di eseguire il turno. In genere, ciò darebbe tre istruzioni (confronto, ramo, spostamento). (Certo, in questo caso non sarebbe necessario in quanto il conteggio dei turni è noto.)

43
Lindydancer

È un comportamento indefinito secondo lo standard C++:

Il valore di E1 << E2 è E1 posizioni bit E2 spostate a sinistra; i bit liberati vengono riempiti con zero. Se E1 ha un tipo senza segno, il valore del risultato è E1 × 2 ^ E2, ridotto di un modulo in più rispetto al valore massimo rappresentabile nel tipo di risultato. Altrimenti, se E1 ha un tipo con segno e un valore non negativo ed E1 × 2 ^ E2 è rappresentabile nel tipo di risultato, allora quello è il valore risultante; altrimenti, il comportamento non è definito.

21
David Heffernan

Le risposte di Lindydancer e 6502 spiegano perché (su alcune macchine) sembra essere un 1 in fase di stampa (sebbene il comportamento dell'operazione non sia definito). Sto aggiungendo i dettagli nel caso in cui non siano ovvi.

Suppongo che (come me) tu stia eseguendo il programma su un processore Intel. GCC genera queste istruzioni di assemblaggio per l'operazione di cambio:

movl $32, %ecx
sall %cl, %eax

Sull'argomento di sall e altre operazioni a turni, pagina 624 in Manuale di riferimento set di istruzioni dice:

8086 non maschera il conteggio dei turni. Tuttavia, tutti gli altri processori Intel Architecture (a partire dal processore Intel 286) mascherano il conteggio dei turni su cinque bit, risultando in un conteggio massimo di 31. Questo mascheramento viene eseguito in tutte le modalità operative (inclusa la modalità virtual-8086) per ridurre il tempo massimo di esecuzione delle istruzioni.

Poiché i 5 bit inferiori di 32 sono zero, allora 1 << 32 è equivalente a 1 << 0, che è 1.

Sperimentando numeri più grandi, lo prevediamo

cout << (a << 32) << " " << (a << 33) << " " << (a << 34) << "\n";

stamperebbe 1 2 4, e in effetti è quello che sta succedendo sulla mia macchina.

13
antonakos

Non funziona come previsto perché ti aspetti troppo.

Nel caso di x86, l'hardware non si preoccupa delle operazioni di spostamento in cui il contatore è più grande della dimensione del registro (vedere ad esempio la descrizione dell'istruzione SHL su documentazione di riferimento x86 per una spiegazione).

Lo standard C++ non voleva imporre un costo aggiuntivo dicendo cosa fare in questi casi perché il codice generato sarebbe stato costretto ad aggiungere controlli e logica extra per ogni spostamento parametrico.

Con questa libertà, gli implementatori di compilatori possono generare solo un'istruzione Assembly senza alcun test o ramo.

Un approccio più "utile" e "logico" sarebbe stato ad esempio avere (x << y) equivalente a (x >> -y) e anche la gestione di contatori alti con un comportamento logico e coerente.

Tuttavia, ciò avrebbe richiesto una gestione molto più lenta per lo spostamento dei bit, quindi la scelta era quella di fare ciò che l'hardware fa, lasciando ai programmatori la necessità di scrivere le proprie funzioni per i casi secondari.

Dato che hardware diverso fa cose diverse in questi casi, ciò che dice lo standard è fondamentalmente "Qualsiasi cosa accada quando fai cose strane non incolpare C++, è colpa tua" tradotto in legalese.

9
6502

Spostare una variabile a 32 bit di 32 o più bit è un comportamento indefinito e può far sì che il compilatore faccia volare i demoni dal naso.

Scherzi a parte, la maggior parte delle volte l'output sarà 0 (se int è 32 bit o meno) poiché si sposta l'1 fino a quando non scende di nuovo e non rimane altro che 0. Ma il compilatore potrebbe ottimizzarlo per fare quello che gli piace.

Vedi l'eccellente post sul blog LLVM Cosa ogni programmatore C dovrebbe sapere sul comportamento indefinito , una lettura obbligata per ogni sviluppatore C.

8
DarkDust

Dato che stai spostando un bit di un int di 32 bit; otterrai: warning C4293: '<<' : shift count negative or too big, undefined behavior in VS. Ciò significa che stai spostando oltre il numero intero e la risposta potrebbe essere QUALCOSA, perché è un comportamento indefinito.

5
Bob2Chiv

Potresti provare quanto segue. Questo in realtà dà l'output come 0 dopo 32 turni a sinistra.

#include<iostream>
#include<cstdio>

using namespace std;

int main()
{
  int a = 1;
  a <<= 31;
  cout << (a <<= 1);
  return 0;
}
0
Sandip Agarwal

Ho avuto lo stesso problema e questo ha funzionato per me:

f = ((long long) 1 << (i-1));

Dove posso essere un numero intero maggiore di 32 bit. L'1 deve essere un numero intero a 64 bit affinché lo spostamento funzioni.

0
Pieter888