Perché non ci sono istruzioni `nand` nelle CPU moderne?


52

Perché i progettisti x86 (o anche altre architetture CPU) hanno deciso di non includerlo? È una porta logica che può essere utilizzata per costruire altre porte logiche, quindi è veloce come una singola istruzione. Invece di concatenare note andistruzioni (entrambe sono create da nand), perché nessuna nandistruzione ?.


20
Che caso hai per l'istruzione nand? Probabilmente i designer x86 non ne hanno mai trovato nessuno
PlasmaHH il

16
ARM ha l' BICistruzione, che è a & ~b. Arm Thumb-2 ha l' ORNistruzione che è ~(a | b). ARM è piuttosto moderno. La codifica di un'istruzione nel set di istruzioni della CPU ha i suoi costi. Quindi solo i più "utili" si stanno facendo strada verso l'ISA.
Eugene Sh.

24
@Amumu Potremmo avere ~(((a << 1) | (b >> 1)) | 0x55555555)anche delle istruzioni. Lo scopo sarebbe di ~(((a << 1) | (b >> 1)) | 0x55555555)poterlo tradurre in un'unica istruzione anziché in 6. Quindi, perché no?
user253751

11
@Amumu: non è un caso d'uso, e anche il suo ~ no! Un caso d'uso è una ragione convincente per cui tale istruzione è utile e dove può essere applicata. Il tuo ragionamento è come dire "L'istruzione dovrebbe essere lì in modo che possa essere utilizzata", ma la domanda è "a cosa serve usarlo è così importante che è utile spendere risorse".
PlasmaHH il

4
Ho programmato per 45 anni, scritto alcuni compilatori e usato alcuni operatori logici strani quando disponibili come IMP, ma non ho mai avuto un uso per un operatore o un'istruzione NAND.
user207421

Risposte:


62

http://www.ibm.com/support/knowledgecenter/ssw_aix_61/com.ibm.aix.alangref/idalangref_nand_nd_instrs.htm : POWER ha la NAND.

Ma generalmente le CPU moderne sono costruite per abbinare la generazione di codice automatizzata da parte dei compilatori e NAND bit a bit è molto raramente richiesto. AND bit OR e OR vengono utilizzati più spesso per manipolare campi di bit in strutture di dati. In effetti, SSE ha AND-NOT ma non NAND.

Ogni istruzione ha un costo nella logica di decodifica e consuma un codice operativo che potrebbe essere utilizzato per qualcos'altro. Soprattutto nelle codifiche a lunghezza variabile come x86, è possibile rimanere senza codici operativi brevi e utilizzare quelli più lunghi, che potenzialmente rallentano tutto il codice.


5
@supercat AND-NOT è comunemente usato per disattivare i bit in una variabile set di bit. es.if(windowType & ~WINDOW_RESIZABLE) { ... do stuff for variable-sized windows ... }
adib,

2
@adib: Sì. Una caratteristica interessante di "e non" è che a differenza dell'operatore "bit a bit" [~] la dimensione del risultato non ha importanza. Se fooè un uint64_t, l'istruzione a foo &= ~something;volte può cancellare più bit del previsto, ma se ci fosse un &~=operatore tali problemi potrebbero essere evitati.
supercat

6
@adib if WINDOW_RESIZABLEè una costante, quindi un ottimizzatore dovrebbe valutare ~WINDOW_RESIZABLEin fase di compilazione, quindi questo è solo un AND in fase di esecuzione.
alephzero,

4
@ MarkRansom: No, la causa e l'effetto sono completamente corretti dalla cronologia informatica. Questo fenomeno di progettazione di CPU ottimizzate per i compilatori anziché per i programmatori di assemblaggi umani faceva parte del movimento RISC (tuttavia, il movimento RISC stesso è più ampio di quel solo aspetto). Le CPU progettate per i compilatori includono ARM e Atmel AVR. Alla fine degli anni '90 e nei primi anni del 2000 le persone hanno assunto scrittori di compilatori e programmatori di sistemi operativi per progettare set di istruzioni CPU
slebetman,

3
Oggigiorno le operazioni da registro a registro sono essenzialmente gratuite rispetto all'accesso alla RAM. L'implementazione di istruzioni ridondanti costa il silicio immobiliare nella CPU. Quindi di solito ci sarà solo una forma di bit-OR e bit-bit-E poiché l'aggiunta di un'operazione di registro-complemento bit-complemento difficilmente rallenterà mai nulla.
nigel222,

31

Il costo di tali funzioni ALU è

1) la logica che svolge la funzione stessa

2) il selettore che seleziona questo risultato della funzione anziché gli altri tra tutte le funzioni ALU

3) il costo di avere questa opzione nel set di istruzioni (e non avere qualche altra utile funzione)

Sono d'accordo con te che il 1) costo è molto piccolo. Il costo 2) e 3) è tuttavia quasi indipendente dalla funzione. Penso che in questo caso i 3) costi (i bit occupati nell'istruzione) fossero la ragione per non avere questa istruzione specifica. I bit in un'istruzione sono una risorsa molto scarsa per un progettista di CPU / architettura.


29

Giralo - per prima cosa vedi perché Nand era popolare nella progettazione della logica hardware - ha diverse proprietà utili lì. Quindi chiedi se quelle proprietà sono ancora applicabili in un'istruzione CPU ...

TL / DR: non lo fanno, quindi non ci sono svantaggi nell'utilizzo di And, Or or Not.

Il più grande vantaggio della logica Nand cablata era la velocità, ottenuta riducendo il numero di livelli logici (stadi transistor) tra gli ingressi e le uscite di un circuito. In una CPU, la velocità di clock è determinata dalla velocità di operazioni molto più complesse come l'aggiunta, quindi accelerare un'operazione AND non consente di aumentare la frequenza di clock.

E il numero di volte in cui devi combinare altre istruzioni è minuziosamente piccolo, abbastanza da non far spazio a Nand nel set di istruzioni.


1
Nei casi in cui non è richiesto l'isolamento dell'input, "e non" sembrerebbe molto economico nell'hardware. Nel 1977 ho progettato un controller di segnale di svolta per il rimorchio dei miei genitori usando due transistor e due diodi per luce per eseguire una funzione "XOR" [lampada sinistra == xor (segnale sinistro, freno); right lamp == xor (segnale destro, freno)], essenzialmente cablando o usando due funzioni e-non per ogni luce. Non ho visto tali trucchi usati nella progettazione LSI, ma penserei che in TTL o NMOS, nei casi in cui qualunque cosa alimenta un input avrebbe una capacità di azionamento adeguata, tali trucchi potrebbero salvare i circuiti.
supercat

12

Vorrei essere d'accordo con Brian qui, Wouter e PJC50.

Vorrei anche aggiungere che per scopi generici, in particolare CISC, processori, istruzioni non hanno tutti gli stessi throughput - un'operazione complicata potrebbe richiedere semplicemente più cicli di una semplice.

Considera X86: AND(che è un'operazione "e") è probabilmente molto veloce. Lo stesso vale per NOT. Diamo un'occhiata a un po 'di smontaggio:

Codice di input:

#include <immintrin.h>
#include <stdint.h>

__m512i nand512(__m512i a, __m512i b){return ~(a&b);}
__m256i nand256(__m256i a, __m256i b){return ~(a&b);}
__m128i nand128(__m128i a, __m128i b){return ~(a&b);}
uint64_t nand64(uint64_t a, uint64_t b){return ~(a&b);}
uint32_t nand32(uint32_t a, uint32_t b){return ~(a&b);}
uint16_t nand16(uint16_t a, uint16_t b){return ~(a&b);}
uint8_t nand8(uint8_t a, uint8_t b){return ~(a&b);}

Comando per produrre assemblaggio:

gcc -O3 -c -S  -mavx512f test.c

Gruppo di uscita (abbreviato):

    .file   "test.c"
nand512:
.LFB4591:
    .cfi_startproc
    vpandq  %zmm1, %zmm0, %zmm0
    vpternlogd  $0xFF, %zmm1, %zmm1, %zmm1
    vpxorq  %zmm1, %zmm0, %zmm0
    ret
    .cfi_endproc
nand256:
.LFB4592:
    .cfi_startproc
    vpand   %ymm1, %ymm0, %ymm0
    vpcmpeqd    %ymm1, %ymm1, %ymm1
    vpxor   %ymm1, %ymm0, %ymm0
    ret
    .cfi_endproc
nand128:
.LFB4593:
    .cfi_startproc
    vpand   %xmm1, %xmm0, %xmm0
    vpcmpeqd    %xmm1, %xmm1, %xmm1
    vpxor   %xmm1, %xmm0, %xmm0
    ret
    .cfi_endproc
nand64:
.LFB4594:
    .cfi_startproc
    movq    %rdi, %rax
    andq    %rsi, %rax
    notq    %rax
    ret
    .cfi_endproc
nand32:
.LFB4595:
    .cfi_startproc
    movl    %edi, %eax
    andl    %esi, %eax
    notl    %eax
    ret
    .cfi_endproc
nand16:
.LFB4596:
    .cfi_startproc
    andl    %esi, %edi
    movl    %edi, %eax
    notl    %eax
    ret
    .cfi_endproc
nand8:
.LFB4597:
    .cfi_startproc
    andl    %esi, %edi
    movl    %edi, %eax
    notl    %eax
    ret
    .cfi_endproc

Come si può vedere, per i tipi di dati sub-64-dimensioni, le cose sono semplicemente tutte gestite come anela (da qui il e l e non l ), dato che è il bitwidth "nativo" del mio compilatore, come sembra.

Il fatto che ci sia movuna via di mezzo è dovuto solo al fatto che eaxè il registro che contiene il valore restituito di una funzione. Normalmente, si calcola semplicemente nel ediregistro generale per calcolare con il risultato.

Per 64 bit, è lo stesso - solo con qparole "quad" (quindi, finali ) e rax/ rsiinvece di eax/ edi.

Sembra che per operandi a 128 bit e superiori, Intel non si preoccupasse di implementare un'operazione "non"; invece, il compilatore produce un 1registro completo (auto-confronto del registro con se stesso, risultato memorizzato nel registro con l' vdcmpeqdistruzione), ed xorè quello.

In breve: implementando un'operazione complicata con più istruzioni elementari, non devi necessariamente rallentare l'operazione - semplicemente non c'è alcun vantaggio ad avere un'istruzione che fa il lavoro di più istruzioni se non è più veloce.


10

Prima di tutto non confondere le operazioni logiche e bit per bit.

Le operazioni bit a bit vengono generalmente utilizzate per impostare / cancellare / attivare / verificare i bit nei campi di bit. Nessuna di queste operazioni richiede nand ("e non", noto anche come "bit clear" è più utile).

Le operazioni logiche nella maggior parte dei moderni linguaggi di programmazione sono valutate usando la logica di corto circuito. Quindi di solito è necessario un approccio basato sul ramo per implementarli. Anche quando il compilatore può determinare che il cortocircuito rispetto alla valutazione completa non fa alcuna differenza per il comportamento del programma, gli operandi per le operazioni logiche di solito non sono in una forma conveniente per implementare l'espressione usando le operazioni asm bit a bit.


10

La NAND spesso non viene implementata direttamente perché avere l'istruzione AND implicitamente ti dà la possibilità di saltare su una condizione NAND.

L'esecuzione di un'operazione logica in una CPU spesso imposta bit in un registro flag.

La maggior parte dei registri di bandiera ha una bandiera ZERO. Il flag zero viene impostato se il risultato di un'operazione logica è zero e viene cancellato diversamente.

La maggior parte delle CPU moderne ha un'istruzione di salto che salta se è impostato il flag zero. Hanno anche un'istruzione che salta se la bandiera zero non è impostata.

AND e NAND sono complementi. Se il risultato di un'operazione AND è zero, il risultato di un'operazione NAND è 1 e viceversa.

Quindi, se vuoi saltare se la NAND di due valori è vera, esegui semplicemente l'operazione AND e salta se è impostato il flag zero.

Quindi, se vuoi saltare se la NAND di due valori è falsa, esegui semplicemente l'operazione AND e salta se il flag zero è chiaro.


In effetti, la scelta dell'istruzione di salto condizionale offre una scelta di logica di inversione e non inversione per un'intera classe di operazioni, senza dover implementare quella scelta per ciascuna individualmente.
Chris Stratton,

Questa avrebbe dovuto essere la risposta migliore. Le operazioni con flag zero rendono NAND superflua per le operazioni logiche in quanto AND + JNZ e AND + JZ sono essenzialmente in cortocircuito / logico AND e NAND rispettivamente, entrambi accettano lo stesso numero di codice operativo.
Sdraiati Ryan il

4

Solo perché qualcosa è economico non significa che sia conveniente .

Se prendiamo la tua argomentazione ad assurdo, arriveremmo alla conclusione che una CPU dovrebbe essere composta principalmente da centinaia di versioni di istruzioni NOP, perché sono le più economiche da implementare.

O confrontalo con strumenti finanziari: compreresti un'obbligazione da $ 1 con un rendimento dello 0,01% solo perché puoi? No, preferiresti risparmiare quei dollari fino a quando non avrai abbastanza per comprare un'obbligazione da $ 10 con un rendimento migliore. Lo stesso vale per il budget in silicone su una CPU: è efficace per asportare molte operazioni economiche ma inutili come la NAND e mettere i transistor salvati in qualcosa di più costoso ma davvero utile.

Non c'è gara per avere quante più operazioni possibili. Poiché RISC vs CISC avevano dimostrato ciò che Turing sapeva fin dall'inizio: meno è di più. In realtà è meglio avere meno operazioni possibili.


nopnon può attuare tutte le altre porte logiche, ma nando norpuò, ricreare efficacemente qualsiasi istruzione che viene implementata in una CPU nel software. Se prendiamo l'approccio RISC, cioè ...
Amumu,

@Amumu Penso che tu stia mescolando gatee instruction. I gate vengono utilizzati per implementare le istruzioni, non viceversa. NOPè un'istruzione, non un cancello. E sì, le CPU contengono migliaia o forse anche milioni di porte NAND per implementare tutte le istruzioni. Non solo l'istruzione "NAND".
Agent_L

2
@Amumu Questo non è l'approccio RISC :) Questo è l'approccio "usa le astrazioni più ampie", che non è troppo utile al di fuori di applicazioni molto specifiche. Certo, nandè un cancello che può essere utilizzato per implementare altre porte; ma hai già tutte le altre istruzioni . Reimplementarli usando nandun'istruzione sarebbe più lento . E sono usati troppo spesso per tollerarlo, a differenza del tuo esempio specifico selezionato ciliegia dove nandprodurrebbe codice più breve (codice non più veloce , solo più breve); ma è estremamente raro e il vantaggio non vale semplicemente il costo.
Luaan,

@Amumu Se avessimo usato il tuo approccio, non avremmo avuto numeri posizionali. Qual è il punto in cui puoi semplicemente dire ((((()))))invece di 5, giusto? Cinque è solo un numero specifico, è troppo limitante - i set sono molto più generali: P
Luaan

@Agent_L Sì, conosco le istruzioni per l'implementazione di gates. nandimplementa tutte le porte, quindi implicitamente nandpuò implementare tutte le altre istruzioni. Quindi, se un programmatore ha a nanddisposizione un'istruzione, può inventare le proprie istruzioni quando pensa nelle porte logiche. Ciò che intendevo sin dall'inizio è che se è così fondamentale, perché non gli sono state date le proprie istruzioni (cioè un codice operativo nella logica del decodificatore), quindi un programmatore può usare tale istruzione. Naturalmente dopo che ho ricevuto risposta, ora so che dipende dall'uso del software.
Amumu,

3

A livello hardware, né nand né l'operazione logica elementare. A seconda della tecnologia (o in base a ciò che chiami arbitrariamente 1 e a ciò che chiami 0), né nand né né possono essere implementati in un modo molto semplice ed elementare.

Se ignoriamo il caso "né", tutte le altre logiche sono costruite da nand. Ma non perché ci sia qualche prova informatica che tutte le operazioni logiche possono essere costruite da e - la ragione è che non esiste alcun metodo elementare per costruire xor, o, e così via, che sia meglio che costruirlo da nand.

Per le istruzioni del computer, la situazione è diversa. Potrebbe essere implementata un'istruzione nand, e sarebbe un po 'più economica rispetto all'implementazione di xor, per esempio. Ma solo un pochino, perché la logica che calcola il risultato è minuscola rispetto alla logica che decodifica l'istruzione, sposta gli operandi, si assicura che venga calcolata una sola operazione, raccoglie il risultato e lo consegna nel posto giusto. Ogni istruzione richiede un ciclo per essere eseguita, come un'aggiunta che è dieci volte più complicata in termini di logica. Il risparmio di nand vs. xor sarebbe trascurabile.

Ciò che conta quindi è quante istruzioni sono necessarie per le operazioni che vengono effettivamente eseguite dal codice tipico . Nand non si trova da nessuna parte in cima all'elenco delle operazioni comunemente richieste. È molto più comune che e, o, non siano richiesti. I progettisti di processori e set di istruzioni esamineranno un sacco di codice esistente e determineranno come le diverse istruzioni influenzerebbero quel codice. Molto probabilmente hanno scoperto che l'aggiunta di un'istruzione nand porterebbe a una riduzione molto ridotta del numero di istruzioni del processore in esecuzione per eseguire il codice tipico e la sostituzione di alcune istruzioni esistenti con nand aumenterebbe il numero di istruzioni eseguite.


2

Solo perché NAND (o NOR) può implementare tutte le porte nella logica combinatoria, non si traduce in un efficiente operatore bit a bit allo stesso modo. Per implementare un AND usando solo le operazioni NAND, dove c = a AND b, dovresti avere c = a NAND b, quindi b = -1, quindi c = c NAND b (per un NOT). Le operazioni bit per bit della logica sono AND, OR, EOR, NOT, NAND e NEOR. Non è molto da trattare, e i primi quattro sono generalmente integrati comunque. Nella logica combinatoria, i circuiti logici di base sono limitati solo dal numero di porte disponibili, che è un gioco di palla completamente diverso. Il numero di possibili interconnessioni in un array di gate programmabile, che suona come quello che stai veramente cercando, sarebbe davvero un numero molto grande. Alcuni processori hanno effettivamente array di gate integrati.


0

Non si implementa una porta logica solo perché ha completezza funzionale, specialmente se le altre porte logiche sono disponibili in modo nativo. Implementate ciò che tende ad essere maggiormente utilizzato dai compilatori.

NAND, NOR e XNOR sono molto raramente necessari. Oltre agli operatori bit per bit classici AND, OR e XOR, solo ANDN ( ~a & b) - che non è NAND ( ~(a & b)) - avrebbe un'utilità pratica. Se esiste, una CPU dovrebbe implementarlo (e in effetti alcune CPU implementano ANDN).

Per spiegare l'utilità pratica di ANDN, immagina di avere una maschera di bit che utilizza molti bit, ma sei interessato solo ad alcuni di quelli, che sono i seguenti:

enum my_flags {
    IT_IS_FRIDAY = 1,
    ...
    IT_IS_WARM = 8,
    ...
    THE_SUN_SHINES = 64,
    ...
};

Normalmente si desidera verificare se i bit di interesse per la maschera di bit sono

  1. Sono tutti pronti
  2. Almeno uno è impostato
  3. Almeno uno non è impostato
  4. Nessuno è impostato

Cominciamo raccogliendo insieme i tuoi pezzi di interesse:

#define BITS_OF_INTEREST (IT_IS_FRIDAY | IT_IS_WARM | THE_SUN_SHINES)

1. Vengono impostati tutti i bit di interesse: AND bit + NOT logico

Diciamo che vuoi sapere se i tuoi bit di interesse sono tutti impostati. Puoi vederlo come (my_bitmask & IT_IS_FRIDAY) && (my_bitmask & IT_IS_WARM) && (my_bitmask & THE_SUN_SHINES). Comunque normalmente lo comprimerai in

unsigned int life_is_beautiful = !(~my_bitmask & BITS_OF_INTEREST);

2. È impostato almeno un bit di interesse: bit a bit AND

Ora diciamo che vuoi sapere se è impostato almeno un po 'di interesse. Puoi vederlo come (my_bitmask & IT_IS_FRIDAY) || (my_bitmask & IT_IS_WARM) || (my_bitmask & THE_SUN_SHINES). Comunque normalmente lo comprimerai in

unsigned int life_is_not_bad = my_bitmask & BITS_OF_INTEREST;

3. Non è impostato almeno un bit di interesse : AND bit per bit

Ora diciamo che vuoi sapere se non è impostato almeno un po 'di interesse . Puoi vederlo come !(my_bitmask & IT_IS_FRIDAY) || !(my_bitmask & IT_IS_WARM) || !(my_bitmask & THE_SUN_SHINES). Comunque normalmente lo comprimerai in

unsigned int life_is_imperfect = ~my_bitmask & BITS_OF_INTEREST;

4. Nessun bit di interesse impostato: bit a bit AND + logico NOT

Ora diciamo che vuoi sapere se non sono impostati tutti i bit di interesse . Puoi vederlo come !(my_bitmask & IT_IS_FRIDAY) && !(my_bitmask & IT_IS_WARM) && !(my_bitmask & THE_SUN_SHINES). Comunque normalmente lo comprimerai in

unsigned int life_is_horrible = !(my_bitmask & BITS_OF_INTEREST);

Queste sono le operazioni più comuni eseguite su una maschera di bit, oltre ai classici bit a bit OR e XOR. Penso però che una lingua (che non è una CPU ) dovrebbe includere il NAND bit a bit, NOR e operatori XNOR (i cui simboli sarebbe ~&, ~|e ~^), nonostante raramente usato. Non includerei l'operatore ANDN in una lingua, poiché non è commutativo ( a ANDN bnon è lo stesso di b ANDN a) - meglio scrivere ~a & binvece che a ANDN b, il primo mostra più chiaramente l'asimmetria dell'operazione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.