Qual è il modo migliore (quando le prestazioni contano) di implementare una macchina a stati in C #?

StackOverflow https://stackoverflow.com/questions/1617894

Domanda

Ho trovato le seguenti opzioni:

Utilizzo dell'istruzione goto:

Start:
    goto Data
Data:
    goto Finish
Finish:
    ;

usando l'istruzione switch:

switch(m_state) {
    case State.Start:
        m_state = State.Data;
        break;
    case State.Data:            
        m_state = State.Finish;
        break;
    case State.Finish:
        break;
}

usando goto e passa insieme:

switch(m_state) {
    case State.Start:
        goto case State.Data2;
    case State.Data1:
        goto case State.Finish;
    case State.Data2:
        m_state = State.Data1;
        //call to a function outside the state machine
        //that could possibly change the state
        break;
    case State.Finish:
        break;
}

Preferisco la prima opzione usando l'istruzione goto, perché è più veloce e meno dettagliata. Ma non sono sicuro che sia l'opzione migliore. Le prestazioni sono forse sagge, ma quando si tratta di leggibilità non lo so. Ecco perché faccio questa domanda. Quale opzione preferisci e perché?

È stato utile?

Soluzione

Il vantaggio con il passaggio al goto è che hai lo stato in una variabile, non solo nel puntatore dell'istruzione.

Con il metodo goto la macchina a stati deve essere il ciclo principale che controlla tutto il resto, perché non puoi uscirne perché perdi lo stato.

Con il metodo switch la macchina a stati è isolata e puoi andare ovunque tu voglia gestire gli eventi dall'esterno. Quando torni alla macchina statale, continua da dove hai interrotto. Puoi anche avere più di una macchina a stati in esecuzione fianco a fianco, cosa impossibile con la versione goto.

Non sono sicuro di dove stai andando con la terza alternativa, sembra proprio la prima alternativa con un cambio inutile attorno ad essa.

Altri suggerimenti

Preferisco le funzioni reciprocamente chiamanti / ricorsive. Per adattare il tuo esempio:

returnvalue Start() {
    return Data();
}

returnvalue Data() {
    return Finish();
}

returnvalue Finish() {
    …
}

Teoricamente, questo può essere completamente integrato in modo che l'output del compilatore sia equivalente alla soluzione goto (quindi, stessa velocità). Realisticamente, il compilatore / JITter C # probabilmente non lo farà . Ma poiché la soluzione è molto più leggibile (beh, IMHO), la sostituirei solo con la soluzione goto dopo un benchmark molto attento che dimostra che è in effetti inferiore in termini di velocità o che si verificano overflow dello stack (non in questa semplice soluzione ma gli automi più grandi incontrano questo problema).

Anche allora, sicuramente attenersi alla soluzione goto case . Perché? Perché allora tutta la tua disordinata pasta goto è ben racchiusa in una struttura a blocchi (il blocco switch ) e i tuoi spaghetti non rovineranno il resto del codice, impedendo il Bolognese.

In conclusione : la variante funzionale è chiara ma in generale soggetta a problemi. La soluzione goto è disordinata. Solo goto case offre una soluzione a metà strada pulita ed efficiente. Se la prestazione è davvero fondamentale (e l'automa è il collo di bottiglia), scegli la variante strutturata goto case .

C'è una quarta opzione.

Utilizzare un iteratore per implementare una macchina a stati. Ecco un nice short articolo che mostra come

Tuttavia presenta alcuni svantaggi. Manipolare lo stato dall'esterno dell'iteratore non è possibile.

Inoltre, non sono sicuro che sia molto veloce. Ma puoi sempre fare un test.

Se mai vuoi spezzare la logica di transizione della tua macchina a stati in funzioni separate, puoi farlo solo usando le istruzioni switch.

switch(m_state) {
        case State.Start:
                m_state = State.Data;
                break;
        case State.Data:                        
                m_state = ComputeNextState();
                break;
        case State.Finish:
                break;
} 

È anche più leggibile e il sovraccarico dell'istruzione switch (rispetto a Goto) farà la differenza in termini di prestazioni solo in rare circostanze.

EDIT:

Puoi utilizzare " vai a case " per migliorare le prestazioni di piccole dimensioni:

switch(m_state) {
        case State.Start:
                m_state = State.Data; // Don't forget this line!
                goto case State.Data;
        case State.Data:                        
                m_state = ComputeNextState();
                break;
        case State.Finish:
                break;
} 

Tuttavia si corre il rischio di dimenticare di aggiornare la variabile di stato. Il che potrebbe causare bug sottili in seguito (perché hai presupposto che fosse impostato "m_state"), quindi suggerirei di evitarlo.

Personalmente preferisco il secondo con goto poiché il primo richiederà un passaggio in loop non necessario (ad esempio) per passare al nuovo stato

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top