¿Por qué se usaría el modificador de parámetro "in" en C #?


84

Entonces, (creo que) entiendo lo que hace el inmodificador de parámetro. Pero lo que hace parece ser bastante redundante.

Por lo general, creo que la única razón para usar un refsería modificar la variable de llamada, que está explícitamente prohibida por in. Entonces, pasar por inreferencia parece lógicamente equivalente a pasar por valor.

¿Existe algún tipo de ventaja en el rendimiento? Yo creía que en el lado del back-end de las cosas, un refparámetro debe al menos copiar la dirección física de la variable, que debe ser del mismo tamaño que cualquier referencia de objeto típica.

Entonces, ¿la ventaja es solo en estructuras más grandes, o hay alguna optimización del compilador detrás de escena que lo hace atractivo en otros lugares? Si es lo último, ¿por qué no debería hacer que cada parámetro sea an in?


1
Sí, hay una ventaja de rendimiento. refse utiliza para pasar estructuras por referencia en lugar de copiarlas. insignifica que la estructura no debe modificarse.
Panagiotis Kanavos

@dbc no, esto no tiene nada que ver con la interoperabilidad
Panagiotis Kanavos

4
Rendimiento por tipos de valor. La nueva palabra clave "in" para C # 7.2
Silvermind

1
Discusión detallada aquí . Tenga en cuenta la última advertencia:It means that you should never pass a non-readonly struct as in parameter.
Panagiotis Kanavos

Podría haber jurado que esto era un duplicado, pero ya no puedo encontrarlo
JAD

Respuestas:


80

in se introdujo recientemente en el lenguaje C #.

ines en realidad un ref readonly. En términos generales, solo hay un caso de uso en el que inpuede ser útil: las aplicaciones de alto rendimiento que se ocupan de muchos correos electrónicos grandes readonly struct.

Asumiendo que tienes:

readonly struct VeryLarge
{
    public readonly long Value1;   
    public readonly long Value2;

    public long Compute() { }
    // etc
}

y

void Process(in VeryLarge value) { }

En ese caso, la VeryLargeestructura se pasará por referencia sin crear copias defensivas cuando se utilice esta estructura en el Processmétodo (por ejemplo, al llamar value.Compute()), y el compilador asegura la inmutabilidad de la estructura.

Tenga en cuenta que pasar un not-readonly structcon un inmodificador hará que el compilador cree una copia defensiva cuando llame a los métodos de struct y acceda a las propiedades en el Processmétodo anterior, lo que afectará negativamente el rendimiento.

Hay una entrada de blog de MSDN realmente buena que recomiendo leer con atención.

Si desea obtener más antecedentes históricos de la inintroducción, puede leer esta discusión en el repositorio de GitHub del lenguaje C #.

En general, la mayoría de los desarrolladores están de acuerdo en que la introducción de inpodría verse como un error. Es una característica de lenguaje bastante exótica y solo puede ser útil en casos de borde de alto rendimiento.


¿Suprime las copias defensivas generadas por el compilador, o simplemente elimina la necesidad de que los programadores creen manualmente copias defensivas en contextos que de otro modo usarían ref, por ejemplo, permitiendo someProc(in thing.someProperty);versus propType myProp = thing.someProperty; someProc(ref myProp);? ¿Qué es inun concepto solo de C # outo se ha agregado a .NET Framework?
supercat

@supercat, no puede pasar una propiedad usando in, porque inefectivamente reftiene un atributo especial. Por lo tanto, su primer fragmento no se compilará. @VisualMelon, es cierto, la copia defensiva se produce al llamar a métodos o al acceder a las propiedades de la estructura desde dentro del método que obtiene la estructura como argumento.
dymanoid

@dymanoid perdón por enviarte spam, me tomó alrededor de 10 intentos para entender lo que escribiste (¡y no creo que sea tu culpa!)
VisualMelon

4
@dymanoid: Ambos iny outson conceptos que deberían haber estado en el Framework desde el principio (junto con un medio por el cual los métodos y propiedades podrían indicar si se modifican this). Un compilador podría permitir que se pase una propiedad a un inargumento pasando una referencia a un contenido temporal; si una función escrita en otro idioma modifica ese temporal, la semántica sería un poco asquerosa, pero eso sería culpa de que el comportamiento de la función no coincida con su firma.
supercat

1
@supercat, no abra una discusión aquí (de todos modos, no estoy en el equipo de conceptos de .NET Framework). Hay una larga discusión a la que se hace referencia en la respuesta, y esa discusión de GitHub hace referencia a otras: lectura interesante. Por cierto, puede pasar propiedades por referencia en VB.NET: el compilador crea esos temporales por usted (lo que, por supuesto, puede llevar a problemas oscuros).
dymanoid

41

pasar por inreferencia parece lógicamente equivalente a pasar por valor.

Correcto.

¿Existe algún tipo de ventaja en el rendimiento?

Si.

Yo creía que en el lado del back-end de las cosas, un refparámetro debe al menos copiar la dirección física de la variable, que debe ser del mismo tamaño que cualquier referencia de objeto típica.

No existe el requisito de que una referencia a un objeto y una referencia a una variable sean del mismo tamaño, y no existe el requisito de que ninguna sea del tamaño de una palabra de máquina, pero sí, en la práctica ambas son de 32 bits en 32 máquinas de bits y 64 bits en máquinas de 64 bits.

No tengo claro qué crees que tiene que ver la "dirección física" con esto. En Windows usamos direcciones virtuales , no direcciones físicas en el código de modo de usuario. Tengo curiosidad por saber en qué posibles circunstancias imagina que una dirección física es significativa en un programa de C #.

Tampoco es un requisito que se implemente una referencia de ningún tipo como la dirección virtual del almacenamiento. Las referencias pueden ser identificadores opacos en tablas GC en una implementación conforme a la especificación CLI.

¿La ventaja es solo en estructuras más grandes?

Disminuir el costo de pasar estructuras más grandes es el escenario motivador para la función.

Tenga en cuenta que no hay garantía de que inun programa sea realmente más rápido y puede hacer que los programas sean más lentos. Todas las preguntas sobre desempeño deben ser respondidas mediante investigación empírica . Hay muy pocas optimizaciones que siempre sean ganadoras ; esta no es una optimización de "siempre ganar".

¿Hay alguna optimización del compilador detrás de escena que lo haga atractivo en otros lugares?

El compilador y el tiempo de ejecución pueden realizar cualquier optimización que elijan si hacerlo no infringe las reglas de la especificación C #. Que yo sepa, todavía no existe tal optimización para los inparámetros, pero eso no excluye tales optimizaciones en el futuro.

¿Por qué no debería hacer que todos los parámetros estén incluidos?

Bueno, suponga que crea un intparámetro en lugar de un in intparámetro. ¿Qué costos se imponen?

  • el sitio de la llamada ahora requiere una variable en lugar de un valor
  • la variable no se puede registrar. El esquema de asignación de registros cuidadosamente ajustado del jitter acaba de recibir una llave inglesa.
  • el código en el sitio de la llamada es más grande porque debe tomar una referencia a la variable y ponerla en la pila, mientras que antes simplemente podía empujar el valor a la pila de llamadas
  • un código más grande significa que algunas instrucciones de salto corto pueden haberse convertido ahora en instrucciones de salto largo, por lo que, nuevamente, el código ahora es más grande. Esto tiene efectos colaterales en todo tipo de cosas. Los cachés se llenan antes, el jitter tiene más trabajo que hacer, el jitter puede optar por no realizar ciertas optimizaciones en códigos de mayor tamaño, etc.
  • en el sitio del destinatario, hemos convertido el acceso a un valor en la pila (o registro) en una indirección en un puntero. Ahora, es muy probable que ese puntero esté en la caché, pero aún así, ahora hemos convertido un acceso de una instrucción al valor en un acceso de dos instrucciones.
  • Y así.

Suponga que es un doubley lo cambia a un in double. Nuevamente, ahora la variable no se puede registrar en un registro de punto flotante de alto rendimiento. Esto no solo tiene implicaciones en el rendimiento , ¡también puede cambiar el comportamiento del programa! A C # se le permite hacer aritmética flotante con una precisión superior a 64 bits y, por lo general, solo lo hace si los flotantes se pueden registrar.

Esta no es una optimización gratuita. Tienes que medir su desempeño contra las alternativas. Su mejor opción es simplemente no hacer estructuras grandes en primer lugar, como sugieren las pautas de diseño.


"¿Bajo qué [...] posibles circunstancias se imagina que una dirección física es significativa en un programa C #, [...]". . C # generalmente se usa en sistemas que tienen E / S mapeadas en memoria y cosas como vectores de reinicio, etc. Estoy pensando en cajas de wintel viejas y sencillas aquí. Con procesadores x86 y framebuffers VGA. De acuerdo, normalmente no los manipulamos directamente, pero podríamos, ¿verdad?
OmarL

5
¿Es inrealmente pasar por pasar por valor en todos los casos? Si tenemos f(ref T x, in T y)y fmodifica x, debería observar el mismo cambio ycuando se invoca como f(ref a,a). Lo mismo también se aplica si ftoma un in T yy un delegado que se modificará ycuando se llame. Sin in, la semántica sería diferente, ya yque nunca habría cambiado su valor, creo, ya que sería una copia.
chi

2
Sospecho que el OP significaba "dirección física" de una manera informal, como "el patrón de bits que describe la ubicación de la memoria", contrastando eso con "lo que el backend elija usar para describir dónde encontrar un objeto".
Sneftel

@Wilson: Me encantaría ver un ejemplo de lo que está hablando; No conozco a nadie que intente hacer ese tipo de cosas en C #. A lo largo de los años se ha hablado de un "sistema C #" mediante el cual un lenguaje administrado de alto nivel podría usarse para escribir componentes de sistema de bajo nivel, pero nunca he visto ese código.
Eric Lippert

2
@chi: Eso es correcto. Si juegas juegos peligrosos con alias variable, puedes meterte en problemas. Si te duele cuando haces eso, no lo hagas. La intención de ines representar la noción de "pasar por referencia, solo lectura", que es lógicamente equivalente a pasar por valor cuando te comportas con sensatez .
Eric Lippert

6

Ahi esta. Al pasar a struct, la inpalabra clave permite una optimización donde el compilador solo necesita pasar un puntero, sin el riesgo de que el método cambie el contenido. El último es crítico: evita una operación de copia. En estructuras grandes, esto puede marcar una gran diferencia.


2
Esto es simplemente repetir lo que ya dice la pregunta y no responde a la pregunta real que hace.
Servicio

Solo en estructuras de solo lectura. De lo contrario, el compilador seguirá creando una copia defensiva
Panagiotis Kanavos

1
En realidad no. Confirma que hay un beneficio de rendimiento. Dado que IN se creó en la ejecución de la interpretación a través del idioma, sí, esa ES la razón. PERMITE una optimización (en estructuras de solo lectura).
TomTom

3
@TomTom Again, que la pregunta ya cubrió . Lo que pregunta es: "Entonces, ¿la ventaja es solo en estructuras más grandes, o hay alguna optimización del compilador detrás de escena que lo hace atractivo en otros lugares? Si es lo último, ¿por qué no debería hacer que todos los parámetros sean in?" Tenga en cuenta que no se pregunta si es realmente beneficioso para estructuras más grandes, o simplemente cuándo es beneficioso en absoluto. Se acaba preguntando si es beneficioso para las estructuras más pequeñas (y luego un seguimiento en caso afirmativo). No ha respondido ninguna pregunta.
Servicio

2

Esto se hace debido al enfoque de programación funcional. Uno de los principales principios es que la función no debe tener efectos secundarios, lo que significa que no debe cambiar los valores de los parámetros y debe devolver algún valor. En C # no había forma de pasar estructuras (y tipo de valor) sin ser copiadas solo por referencia, lo que permite cambiar el valor. En swift hay un algoritmo hacky que copia la estructura (sus colecciones son estructuras por cierto) siempre que el método comience a cambiar sus valores. Las personas que usan Swift no son todas conscientes del material de copia. Esta es una buena característica de C # ya que es eficiente en memoria y explícita. Si miras lo nuevoverá que se hacen más y más cosas alrededor de estructuras y matrices en la pila. Y en la declaración solo es necesario para estas características. Hay limitaciones mencionadas en las otras respuestas, pero no es tan esencial para comprender hacia dónde se dirige .net.


2

en él es referencia de solo lectura en c # 7.2

Esto significa que no pasan todo el objeto de pila función similar a la ref caso se pasa única referencia a la estructura

pero el intento de cambiar el valor del objeto produce un error del compilador.

Y sí, esto le permitirá optimizar el rendimiento del código si usa grandes estructuras.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.