¿La recomendación de Microsoft de usar propiedades de C # es aplicable al desarrollo de juegos?

26

Entiendo que a veces necesitas propiedades, como:

public int[] Transitions { get; set; }

o:

[SerializeField] private int[] m_Transitions;
public int[] Transitions { get { return m_Transitions; } set { m_Transitions = value; } }

Pero mi impresión es que en el desarrollo del juego, a menos que tengas una razón para hacerlo de otra manera, todo debería ser lo más rápido posible. Mi impresión de lo que he leído es que Unity 3D no está seguro de acceder en línea a través de propiedades, por lo que usar una propiedad generará una llamada de función adicional.

¿Tengo razón en ignorar constantemente las sugerencias de Visual Studio para no usar campos públicos? (Tenga en cuenta que sí usamos int Foo { get; private set; }cuando hay una ventaja en mostrar el uso previsto).

Soy consciente de que la optimización prematura es mala, pero como dije, en el desarrollo de juegos no se hace algo lento cuando un esfuerzo similar podría acelerarlo.

piojo
fuente
34
Siempre verifique con un generador de perfiles antes de creer que algo es "lento". Me dijeron que algo era "lento" y el perfilador me dijo que tenía que ejecutar 500,000,000 de veces para perder medio segundo. Como nunca se ejecutaría más de unas cientos de veces en un cuadro, decidí que era irrelevante.
Almo
55
He descubierto que un poco de abstracción aquí y allá ayuda a aplicar optimizaciones de bajo nivel más ampliamente. Por ejemplo, he visto numerosos proyectos en C simples que utilizan varios tipos de listas enlazadas, que son terriblemente lentas en comparación con las matrices contiguas (por ejemplo, el respaldo para ArrayList). Esto se debe a que C no tiene genéricos, y es probable que a estos programadores de C les sea más fácil volver a implementar listas enlazadas que hacer lo mismo para ArrayListsel algoritmo de cambio de tamaño. Si encuentra una buena optimización de bajo nivel, ¡encapsúlela!
hegel5000
3
@Almo ¿Aplica esa misma lógica a las operaciones que ocurren en 10,000 lugares diferentes? Porque ese es el acceso de miembros de la clase. Lógicamente, debería multiplicar el costo de rendimiento por 10K antes de decidir que la clase de optimización no importa. Y 0,5 ms es una mejor regla general para cuando el rendimiento de tu juego se arruinará, no medio segundo. Su punto sigue en pie, aunque no creo que la acción correcta sea tan clara como parece.
piojo
55
Tienes 2 caballos. Compite con ellos.
Mástil
@piojo no lo hago. Algún pensamiento siempre debe entrar en estas cosas. En mi caso, fue para bucles en el código de la interfaz de usuario. Una instancia de IU, pocos bucles, pocas iteraciones. Para el registro, he votado tu comentario. :)
Almo

Respuestas:

44

Pero mi impresión es que en el desarrollo del juego, a menos que tengas una razón para hacerlo de otra manera, todo debería ser lo más rápido posible.

No necesariamente. Al igual que en el software de aplicación, hay un código en un juego que es crítico para el rendimiento y un código que no lo es.

Si el código se ejecuta varios miles de veces por cuadro, entonces tal optimización de bajo nivel podría tener sentido. (Aunque es posible que primero desee verificar primero si es realmente necesario llamarlo con tanta frecuencia. El código más rápido es el código que no ejecuta)

Si el código se ejecuta una vez cada dos segundos, la legibilidad y la facilidad de mantenimiento son mucho más importantes que el rendimiento.

He leído que Unity 3D no está seguro de acceder en línea a través de propiedades, por lo que usar una propiedad dará como resultado una llamada de función adicional.

Con propiedades triviales al estilo de int Foo { get; private set; }usted, puede estar bastante seguro de que el compilador optimizará el contenedor de propiedades. Pero:

  • si realmente tiene una implementación, ya no puede estar tan seguro. Cuanto más compleja sea esa implementación, menos probable será que el compilador pueda incorporarla.
  • Las propiedades pueden ocultar la complejidad. Esta es una espada de doble filo. Por un lado, hace que su código sea más legible y modificable. Pero, por otro lado, otro desarrollador que acceda a una propiedad de su clase podría pensar que solo está accediendo a una sola variable y no darse cuenta de cuánto código ha ocultado realmente detrás de esa propiedad. Cuando una propiedad comienza a ser computacionalmente costosa, entonces tiendo a refactorizarla en un método GetFoo()/ explícito SetFoo(value): para dar a entender que están sucediendo muchas más cosas detrás de escena. (o si necesito estar aún más seguro de que otros entiendan: CalculateFoo()/ SwitchFoo(value))
Philipp
fuente
Acceder a un campo dentro de un campo público no de solo lectura de tipo de estructura es rápido, incluso si ese campo está dentro de una estructura que está dentro de una estructura, etc., siempre que el objeto de nivel superior sea una clase que no sea de solo lectura campo o una variable independiente no de solo lectura. Las estructuras pueden ser bastante grandes sin afectar negativamente el rendimiento si se aplican estas condiciones. Romper este patrón causará una desaceleración proporcional al tamaño de la estructura.
supercat
55
"entonces tiendo a refactorizarlo en un método GetFoo () / SetFoo (valor) explícito:" - Buen punto, tiendo a mover las operaciones pesadas de las propiedades a los métodos.
Mark Rogers
¿Hay alguna manera de confirmar que el compilador Unity 3D realiza la optimización que usted menciona (en las compilaciones IL2CPP y Mono para Android)?
piojo
99
@piojo Como con la mayoría de las preguntas de rendimiento, la respuesta más simple es "simplemente hazlo". Tiene el código listo: pruebe la diferencia entre tener un campo y tener una propiedad. Solo vale la pena investigar los detalles de implementación cuando realmente puede medir una diferencia importante entre los dos enfoques. En mi experiencia, si escribes todo para que sea lo más rápido posible, limitas las optimizaciones de alto nivel disponibles y solo intentas competir en las partes de bajo nivel ("no puedo ver el bosque por los árboles"). Por ejemplo, encontrar formas de omitir por Updatecompleto le ahorrará más tiempo que hacerlo Updaterápido.
Luaan
9

En caso de duda, utilice las mejores prácticas.

Estas en duda


Esa fue la respuesta fácil. La realidad es más compleja, por supuesto.

Primero, existe el mito de que la programación de juegos es una programación de ultra alto rendimiento y todo tiene que ser lo más rápido posible. Clasifico la programación de juegos como la programación cuenta el rendimiento . El desarrollador debe ser consciente de las limitaciones de rendimiento y trabajar dentro de ellas.

En segundo lugar, existe el mito de que hacer que todo sea lo más rápido posible es lo que hace que un programa se ejecute rápido. En realidad, identificar y optimizar los cuellos de botella es lo que hace que un programa sea rápido, y eso es mucho más fácil / más barato / más rápido si el código es fácil de mantener y modificar; El código extremadamente optimizado es difícil de mantener y modificar. De esto se deduce que para escribir programas extremadamente optimizados, debe usar la optimización de código extrema tan a menudo como sea necesario y tan raramente como sea posible.

En tercer lugar, el beneficio de las propiedades y los accesores es que son robustos para cambiar y, por lo tanto, hacen que el código sea más fácil de mantener y modificar, que es algo que necesita para escribir programas rápidos. ¿Desea lanzar una excepción cada vez que alguien quiera establecer su valor en nulo? Usa un setter. ¿Desea enviar una notificación cada vez que cambia el valor? Usa un setter. ¿Quieres registrar el nuevo valor? Usa un setter. ¿Quieres hacer una inicialización perezosa? Usa un captador.

Y cuarto, personalmente no sigo las mejores prácticas de Microsoft en este caso. Si necesito una propiedad con captadores y establecedores triviales y públicos , simplemente uso un campo y simplemente lo cambio a una propiedad cuando lo necesito. Sin embargo, muy rara vez necesito una propiedad tan trivial: es mucho más común que quiera verificar las condiciones de contorno o que desee que el setter sea privado.

Peter - Unban Robert Harvey
fuente
2

Es muy fácil para el tiempo de ejecución .net incorporar propiedades simples, y a menudo lo hace. Pero, esta alineación normalmente está deshabilitada por los perfiladores, por lo tanto, es fácil dejarse engañar y pensar que cambiar una propiedad pública a un campo público acelerará el software.

En el código invocado por otro código que no se volverá a compilar cuando se recompila su código, existe un buen caso para usar propiedades, ya que cambiar de un campo a una propiedad es un cambio radical. Pero para la mayoría de los códigos, aparte de poder establecer un punto de interrupción en get / set, nunca he visto beneficios al usar una propiedad simple en comparación con un campo público.

La razón principal por la que utilicé propiedades en mis 10 años como programador profesional de C # fue para evitar que otros programadores me dijeran que no cumplía con el estándar de codificación. Esta fue una buena compensación, ya que casi nunca había una buena razón para no usar una propiedad.

Ian Ringrose
fuente
2

Las estructuras y los campos desnudos facilitarán la interoperabilidad con algunas API no administradas. A menudo encontrará que la API de bajo nivel desea acceder a los valores por referencia, lo que es bueno para el rendimiento (ya que evitamos una copia innecesaria). El uso de propiedades es un obstáculo para eso y, a menudo, las bibliotecas de contenedor harán copias para facilitar su uso y, a veces, por seguridad.

Debido a eso, a menudo obtendrá un mejor rendimiento con tipos de vectores y matrices que no tienen propiedades sino campos desnudos.


Las mejores prácticas no se están creando en el vacío. A pesar de cierto culto a la carga, en general las mejores prácticas existen por una buena razón.

En este caso, tenemos un par:

  • Una propiedad le permite cambiar la implementación sin cambiar el código del cliente (a nivel binario, es posible cambiar un campo a una propiedad sin cambiar el código del cliente a nivel fuente, sin embargo, se compilará en algo diferente después del cambio) . Eso significa que, al usar una propiedad desde el principio, el código que hace referencia al suyo no tendrá que volver a compilarse solo para cambiar lo que la propiedad hace internamente.

  • Si no todos los valores posibles de los campos de su tipo son estados válidos, entonces no desea exponerlos al código del cliente para que el código lo modifique. Por lo tanto, si algunas combinaciones de valores no son válidas, desea mantener los campos privados (o internos).

He estado diciendo el código del cliente. Eso significa código que llama al tuyo. Si no está creando una biblioteca (o incluso está creando una biblioteca pero está usando una función interna en lugar de una pública), generalmente puede salirse con la suya y tener buena disciplina. En esa situación, la mejor práctica de usar propiedades es evitar que te dispares en el pie. Además, es mucho más fácil razonar sobre el código si puede ver todos los lugares donde un campo puede cambiar en un solo archivo, en lugar de tener que preocuparse por lo que sea que esté siendo modificado o no en otro lugar. De hecho, las propiedades también son buenas para poner puntos de interrupción cuando se da cuenta de lo que salió mal.


Sí, hay valor en ver lo que se está haciendo en la industria. Sin embargo, ¿tiene una motivación para ir en contra de las mejores prácticas? ¿O simplemente va en contra de las mejores prácticas, haciendo que el código sea más difícil de razonar, simplemente porque alguien más lo hizo? Ah, por cierto, "otros lo hacen" es cómo iniciar un culto de carga.


Entonces ... ¿Tu juego corre lento? Es mejor dedicar tiempo a descubrir el cuello de botella y arreglarlo, en lugar de especular sobre lo que podría ser. Puede estar seguro de que el compilador hará muchas optimizaciones, debido a eso, es probable que esté buscando el problema incorrecto.

Por otro lado, si está decidiendo qué hacer para comenzar, debe preocuparse por los algoritmos y las estructuras de datos primero, en lugar de preocuparse por detalles más pequeños, como los campos frente a las propiedades.


Finalmente, ¿ganas algo al ir en contra de las mejores prácticas?

Para los detalles de su caso (Unity y Mono para Android), ¿Unity toma los valores por referencia? Si no lo hace, copiará los valores de todos modos, no habrá ganancia de rendimiento allí.

Si lo hace, si está pasando estos datos a una API que toma ref. ¿Tiene sentido hacer público el campo, o podría hacer que el tipo pueda llamar a la API directamente?

Sí, por supuesto, podría haber optimizaciones que podría hacer mediante el uso de estructuras con campos desnudos. Por ejemplo, puede acceder a ellos con punteros Span<T> o similar. También son compactos en memoria, lo que facilita su serialización para enviarlos a través de la red o almacenarlos permanentemente (y sí, esas son copias).

Ahora, si ha elegido los algoritmos y estructuras correctos, si resultan ser un cuello de botella, entonces decide cuál es la mejor manera de solucionarlo ... que podrían ser estructuras con campos desnudos o no. Podrá preocuparse por eso si y cuando suceda. Mientras tanto, puede preocuparse por asuntos más importantes como hacer que un juego bueno o divertido valga la pena.

Theraot
fuente
1

... mi impresión es que en el desarrollo del juego, a menos que tengas una razón para hacerlo, todo debería ser lo más rápido posible.

:

Soy consciente de que la optimización prematura es mala, pero como dije, en el desarrollo de juegos no se hace algo lento cuando un esfuerzo similar podría acelerarlo.

Tiene razón al saber que la optimización prematura es mala, pero eso es solo la mitad de la historia (vea la cita completa a continuación). Incluso en el desarrollo del juego, no todo debe ser lo más rápido posible.

Primero haz que funcione. Solo entonces, optimice los bits que lo necesitan. Eso no quiere decir que el primer borrador pueda ser desordenado o innecesariamente ineficiente, pero que la optimización no es una prioridad en esta etapa.

" El verdadero problema es que los programadores han pasado demasiado tiempo preocupándose por la eficiencia en los lugares equivocados y en los momentos equivocados ; la optimización prematura es la raíz de todo mal (o al menos la mayor parte) en la programación". Donald Knuth, 1974.

Vincent O'Sullivan
fuente
1

Para cosas básicas como esa, el optimizador de C # lo hará bien de todos modos. Si le preocupa (y si le preocupa más del 1% de su código, lo está haciendo mal ), se creó un atributo para usted. El atributo [MethodImpl(MethodImplOptions.AggressiveInlining)]aumenta en gran medida la posibilidad de que un método esté en línea (los captadores y establecedores de propiedades son métodos).

Joshua
fuente
0

Exponer miembros de tipo estructura como propiedades en lugar de campos a menudo producirá un rendimiento y una semántica deficientes, especialmente en los casos en que las estructuras se tratan realmente como estructuras, en lugar de como "objetos extravagantes".

Una estructura en .NET es una colección de campos unidos entre sí y que, para algunos fines, pueden tratarse como una unidad. .NET Framework está diseñado para permitir que las estructuras se utilicen de la misma manera que los objetos de clase, y en ocasiones esto puede ser conveniente.

Gran parte de los consejos que rodean las estructuras se basan en la noción de que los programadores querrán usar estructuras como objetos, en lugar de colecciones de campos pegadas. Por otro lado, hay muchos casos en los que puede ser útil tener algo que se comporte como un conjunto de campos pegados. Si eso es lo que uno necesita, el consejo de .NET agregará trabajo innecesario para programadores y compiladores por igual, produciendo peor rendimiento y semántica que simplemente usando estructuras directamente.

Los principios más importantes a tener en cuenta al usar estructuras son:

  1. No pase ni devuelva estructuras por valor ni haga que se copien innecesariamente.

  2. Si se cumple el principio n. ° 1, las estructuras grandes funcionarán tan bien como las pequeñas.

Si Alphablobes una estructura que contiene 26 intcampos públicos llamados az, y un captador de propiedades abque devuelve la suma de sus campos ay b, entonces dado Alphablob[] arr; List<Alphablob> list;, el código int foo = arr[0].ab + list[0].ab;necesitará leer los campos ay bde arr[0], pero deberá leer los 26 campos de list[0]aunque ignorar todos menos dos de ellos. Si uno quisiera tener una colección genérica similar a una lista que pudiera funcionar eficientemente con una estructura como alphaBlob, debería reemplazar el getter indexado con un método:

delegate ActByRef<T1,T2>(ref T1 p1, ref T2 p2);
actOnItem<TParam>(int index, ActByRef<T, TParam> proc, ref TParam param);

que luego invocaría proc(ref backingArray[index], ref param);. Dada tal colección, si se reemplaza sum = myCollection[0].ab;con

int result;
myCollection.actOnItem<int>(0, 
  ref (ref alphaBlob item, ref int dest)=>dest = item,
  ref result);

eso evitaría la necesidad de copiar partes del alphaBlobque no se utilizarán en el captador de propiedades. Como el delegado aprobado no accede a nada más que a sus refparámetros, el compilador puede pasar un delegado estático.

Desafortunadamente, .NET Framework no define ninguno de los tipos de delegados necesarios para que esto funcione bien, y la sintaxis termina siendo un desastre. Por otro lado, este enfoque hace posible evitar la copia innecesaria de estructuras, lo que a su vez hará que sea práctico realizar acciones en el lugar en grandes estructuras almacenadas en matrices, que es la forma más eficiente de acceder al almacenamiento.

Super gato
fuente
Hola, ¿publicaste tu respuesta en la página incorrecta?
piojo
@pojo: Quizás debería haber dejado más claro que el propósito de la respuesta es identificar una situación en la que el uso de propiedades en lugar de campos es malo e identificar cómo se puede lograr el rendimiento y las ventajas semánticas de los campos, al tiempo que se encapsula el acceso.
supercat
@piojo: ¿Mi edición aclara las cosas?
supercat
"necesitará leer los 26 campos de la lista [0] a pesar de que ignorará todos menos dos". ¿Qué? ¿Estás seguro? ¿Has revisado el MSIL? C # casi siempre pasa los tipos de clase por referencia.
pjc50
@ pjc50: leer una propiedad produce un resultado por valor. Si tanto el getter indexado listcomo el getter de propiedad abestán alineados, es posible que un JITter reconozca que solo se necesitan miembros ay que bson necesarios, pero no conozco ninguna versión de JITter que realmente haga tales cosas.
supercat