¿Cuál es la mejor manera (cuando el rendimiento importa) de implementar una máquina de estado en C #?

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

Pregunta

Se me ocurrieron las siguientes opciones:

Usando la declaración goto:

Start:
    goto Data
Data:
    goto Finish
Finish:
    ;

usando la instrucción 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 y cambiar juntos:

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;
}

Prefiero la primera opción que usa la instrucción goto, porque es más rápida y menos detallada. Pero no estoy seguro de si es la mejor opción. Tal vez el rendimiento sea inteligente, pero cuando se trata de legibilidad, no lo sé. Es por eso que hago esta pregunta. ¿Qué opción prefieres y por qué?

¿Fue útil?

Solución

La ventaja con el cambio sobre el goto es que tienes el estado en una variable, no solo en el puntero de instrucción.

Con el método goto, la máquina de estado debe ser el bucle principal que controla todo lo demás, porque no puedes salir de él porque perderías el estado.

Con el método de conmutación, la máquina de estado está aislada y puede ir a cualquier lugar que desee para manejar eventos desde el exterior. Cuando regresas a la máquina de estados, simplemente continúa donde lo dejaste. Incluso puede tener más de una máquina de estado funcionando lado a lado, algo que no es posible con la versión goto.

No estoy seguro de a dónde vas con la tercera alternativa, se parece a la primera alternativa con un interruptor inútil a su alrededor.

Otros consejos

Prefiero las funciones recursivas / de llamada mutua. Para adaptar tu ejemplo:

returnvalue Start() {
    return Data();
}

returnvalue Data() {
    return Finish();
}

returnvalue Finish() {
    …
}

Teóricamente, este puede estar completamente en línea para que la salida del compilador sea equivalente a su solución goto (por lo tanto, a la misma velocidad). Realista, el compilador / JIT de C # probablemente no lo hará . Pero como la solución es mucho más legible (bueno, IMHO), solo la reemplazaría con la solución goto después de una evaluación muy cuidadosa que demuestra que es realmente inferior en términos de la velocidad, o de que se produzcan desbordamientos de pila (no en esta solución simple sino en autómatas de mayor tamaño en este problema).

Incluso entonces, me gustaría definitivamente atenerse a la solución goto case . ¿Por qué? Porque entonces toda tu pasta desordenada goto está bien encerrada dentro de una estructura de bloques (el bloque switch ) y tus espaguetis no destrozarán el resto del código, lo que evitará la boloñesa.

En conclusión : la variante funcional es clara pero en general es propensa a problemas. La solución goto está desordenada. Solo goto case ofrece una solución eficiente y a medias. Si el rendimiento es primordial (y el autómata es el cuello de la botella), vaya a la variante estructurada goto case .

Hay una cuarta opción.

Use un iterador para implementar una máquina de estadísticas. Aquí hay un nice short artículo que muestra cómo

Aunque tiene algunas desventajas. No es posible manipular el estado desde fuera del iterador.

Tampoco estoy seguro de si es muy rápido. Pero siempre puedes hacer una prueba.

Si alguna vez desea dividir la lógica de transición de su máquina de estado en funciones separadas, solo puede hacerlo usando instrucciones de cambio.

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

También es más legible, y la sobrecarga de la declaración de cambio (en comparación con Goto) solo hará una diferencia de rendimiento en circunstancias excepcionales.

EDITAR:

Puedes usar " goto case " para hacer pequeñas mejoras de rendimiento:

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;
} 

Sin embargo, corre el riesgo de olvidarse de actualizar la variable de estado. Lo que podría causar errores sutiles más adelante (porque asumió que " m_state " se estableció), por lo que sugeriría evitarlo.

Personalmente, prefiero el segundo con goto, ya que el primero requerirá un paso de bucle innecesario (por ejemplo) para ir al nuevo estado

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top