miércoles, mayo 31, 2006

Rayo de luz

Quicker than a ray of light I'm flying...
Madonna
Como existe el riesgo de aburrir con este tema, voy a ser lo más breve posible. He vuelto a probar una traducción del código de XSight RT a Delphi 7 (nativo, por supuesto). Resultado: tiempos de ejecución casi idénticos, para la versión nativa y para .NET. Sorprendente, ¿verdad?
Esta vez he sido muy cuidadoso con la eficiencia y la calidad del código en Delphi: nada de tipos de interfaz, nada de funciones que devuelvan estructuras, modificación in situ de vectores y colores siempre que sea posible...
¿A qué se puede deber esta vez que no logre más velocidad en Delphi, creando una aplicación nativa? Mi teoría es que tiene que ver con el modelo del procesador. Cuando XSight RT para .NET se ejecuta en mi portátil, el compilador JIT genera código para el Sempron que hace de CPU. Eso implica, por ejemplo, que para copiar un valor de tipo Double, se puede utilizar un registro MMX. Esto lo he comprobado personalmente analizando el código nativo final. En Delphi 7, sin embargo, no existe soporte para CPU's posteriores al Pentium "clásico" (no sé si en las versiones más modernas se permite al compilador usar conjuntos de instrucciones extendidos, pero sospecho que no). Como consecuencia, para copiar un Vector, cada uno de sus tres componentes debe copiarse mediante cuatro instrucciones, usando registros de 32 bits.
Este es un problema importante, que he empezado a ver desde otra óptica. Supongamos, por ejemplo, que aparece una versión de Delphi nativo (o de C++ Builder nativo, o de cualquier otro lenguaje) que, efectivamente, permitiese seleccionar estos conjuntos especiales de instrucciones al compilar... ¡como hace Visual C++ en modo "nativo"! Si quisiéramos aprovechar esa opción, tendríamos que crear tantas versiones de la aplicación como conjuntos de instrucciones incompatibles existen ahora mismo. La aplicación tendría una versión diferente para Pentium y para Athlon, y quizás una versión "legacy" para máquinas viejas. Con .NET, este problema no existe, y lo mejor de todo es lo que acabo de comprobar experimentalmente: la calidad del código generado por el compilador JIT puede ser mejor que la generada por un compilador nativo si no tomamos medidas especiales.
¿Significa esto que XSight RT puede parangonarse ahora mismo con otros ray tracers que funcionan en modo nativo? Probablemente sí. Entonces, ¿por qué me he estado rompiendo el coco todo este tiempo para mejorar la velocidad de ejecución? El problema es que XSight RT implementa una extensión del algoritmo básico de ray tracing llamada informalmente distributed ray tracing. Hablando en plata, el algoritmo extendido implementa trucos especiales para lograr efectos como la distancia focal, la reflexión difusa, etc. Muchos de los ejemplos que utilizo para benchmarks especifican que por cada píxel se lancen de 36 a 144 rayos... aunque muchas veces no sea necesario para obtener la calidad de imagen deseada. Resumiendo: estoy forzando XSight RT, a propósito, para que trabaje más de lo necesario.

martes, mayo 30, 2006

Semana de Argentina y México

Si vives en México o en Argentina, desde hoy, martes 30 de mayo de 2006, hasta el próximo martes 6 de junio, puede adquirir cualquiera de los cursos a distancia de IntSight con un descuento de 15 euros. Este descuento es acumulable con el descuento asociado a la compra simultánea del libro correspondiente. Quiero decir, que si le interesa la oferta de curso sobre ADO.NET más La Cara Oculta de C#, que ya tiene un descuento importante, también le descontamos estos 15 euros. ¿Que por qué? Como en el anuncio de los potingues de L'Oreal (al menos, en España): porque tú lo vales...

Ahora en serio, ¿por qué? Bueno, ¿por qué no? Ya lo he dicho unas cuantas veces: casi la mitad de las visitas a mis páginas vienen del otro lado del charco. Es un recordatorio de que son muchas más cosas las que nos unen que las que nos separan. Y ojalá que los políticos en ambos lados del Atlántico se dedicasen menos a la poesía y la demogagia, e hicieran más por facilitar el comercio entre nuestros países, eliminando trabas absurdas.

Se me ha ocurrido la idea de hacer este tipo de descuentos de cuando en cuando, al azar, sin plan preconcebido: si digo que voy a hacer un descuento a los programadores de tal país tal semana, probablemente la gente postergaría sus compras a esa semana. Por ese mismo motivo, he agrupado dos países esta semana, para que el "ciclo" no sea excesivamente largo.

¿Cómo puede aprovechar esta opción de compra? Escríbame, a mi dirección en el dominio de IntSight, y en respuesta le enviaré las instrucciones.



... y hablando de estadísticas de acceso de mi página, durante mucho tiempo, en la lista de frases introducidas en buscadores, la que más se repetía era "principio de incertidumbre de Heisenberg". Es un artículo del 99 o del 2000, y usaba este principio como símil para hablar sobre el estado observable de una instancia. Parece ser que se produjo algún tipo de retroalimentación en los buscadores, y la frase estuvo en el primer puesto de la lista durante mucho tiempo. ¿Y el truco con más accesos? No lo voy a mencionar por su nombre (por razones que comprenderá enseguida). Es una comparación entre servidores SQL... ¡escrita probablemente a finales del 96 o principios del 97! Naturalmente, a estas alturas la información ha caducado. Sé que tengo que actualizarlo, pero nunca encuentro tiempo.

domingo, mayo 28, 2006

Vídeos para la Serie C

Haga clic para la imagen ampliadaTengo una buena y una mala noticia. La buena: ya está terminado el primer grupo de vídeos para la Serie C del curso de ADO.NET. La serie incluirá dos grupos de vídeos: el primero, utilizando directamente adaptadores de datos, al estilo de .NET v1.1, y el segundo se basará en los nuevos adaptadores de tablas. Mi recomendación es que, incluso si decide usar los adaptadores de tablas, debe saber cómo funcionan los adaptadores de datos. Principalmente, porque los adaptadores de datos siguen estando en el núcleo de la implementación de los adaptadores de tablas. Este ciclo de vídeos se incluirá, a partir de este momento, en los CD's de quienes compren las series A y D, ya terminadas.

La mala noticia: los vídeos ocupan unos 90MB. Esto es demasiado volumen para que lo aguante el servidor de la zona de descargas (por el caudal contratado para el acceso a Internet). No se trata solamente del volumen de cada descarga, sino del total de personas matriculadas en el curso. Mi idea es, cuando estén listas las dos series que faltan (las series B y C), preparar el envío físico, a precio de coste y envío, del curso completo. De momento, no creo que merezca la pena, porque se trata solamente de la mitad de los vídeos que se incluirán finalmente en esta serie.

miércoles, mayo 24, 2006

Sastres y emperadores

Esto se va animando: Nico Aragón ha respondido al artículo ¿Está desnudo el emperador? en su propio blog:
Nico es una de esas personas brillantes que impresiona ver trabajar, y la respuesta tiene enjundia suficiente como para llevarla a "portada". Luego repasaré los comentarios a mi propio artículo porque creo haber visto algunos enlaces interesantes a otras páginas, y de paso añadiré los enlaces a las bitácoras de las personas involucradas (¡un saludo a todos!).

Lo bueno de este tipo de discusiones es que te obliga a refinar tu postura o tu opinión. Confieso que me gusta tirar por elevación, llevando primero el asunto a un extremo: ¿y si X fuese mentira? Claro, este sistema exige que a continuación se corrija la puntería. Si se me ocurre alguna respuesta que aporte algo diferente, naturalmente, la publicaré.

lunes, mayo 22, 2006

Noticias extremadamente breves

  • Hay un grupo de programadores que están editando un boletín sobre Delphi. Luego os doy más detalles. Me han pedido que colabore y he aceptado, aunque personalmente, lo tengo un poco crudo con Delphi, a estas alturas.

  • Creo haber visto que el libro de Jon Shemitz sobre Delphi.NET ya está disponible. Si es así os lo confirmo en cuanto sepa algo más. Ya sabéis lo que opino sobre este "invento", pero os aseguro que el libro está muy bien planteado, y creo que merece la pena incluso olvidándose del lenguaje y quedándose con el material sobre .NET. Más detalles en cuanto sepa más.

Ahora mismo estoy liquidando los nuevos vídeos de apoyo para la serie C del curso de ADO.NET. Estos aparecerán en la zona de descargas como adelanto, en cuanto pasen las pruebas.

jueves, mayo 18, 2006

¿Está desnudo el emperador?

En los comentarios del post Gestores de proyectos, Juanjo plantea un asunto interesante:
¿Que hay de los métodos de analísis, control de versiones, normas de programación, UML, etc..? Nadie nos cuenta nada en ESPAÑOL que no sea pura teoria, algun ejemplo práctico no nos vendría mal. Creo que un blog sobre esto no sería mala idea.
Mi opinión sobre estos temas, en general, es bastante singular. Estoy convencido de que "el emperador está desnudo", en la mayoría de los temas mencionados. Ocurre, sin embargo, que durante todos estos años, he gritado eso mismo unas cuantas veces, refiriéndome a distintos "emperadores", y ha resultado que, con las inevitables excepciones, tenía razón. Esto puede inducir a pensar que me he aficionado al deporte de detectar emperadores en bolas. Estas actitudes son adictivas, hay que reconocerlo. Por estos motivos, tanto por vosotros como por mí, quiero explicar mis razones de mi escepticismo. Prometo enlazar el tema con cualquier refutación que tengáis a bien escribir. Porque en definitiva, si estoy equivocado, soy el primer interesado en saberlo.
Voy a centrarme en UML. ¿Qué es UML? Se supone que es un convenio gráfico para representar determinados aspectos de sistemas. Hasta ahí, todos de acuerdo. Ahora viene la parte que la gente deduce por su cuenta. En realidad, según la persona, se pueden dar por asumidas ciertas "verdades", no necesariamente equivalentes o compatibles entre sí. Por ejemplo:
  • UML debería ser utilizado sistemáticamente en la fase de análisis.
¡Falso! En realidad, el formalismo usado para las distintas fases del proceso de desarrollo debería ser lo más uniforme posible. Esto no es idea mía, por cierto: es Bertrand Meyer 100%. En sus libros, Meyer muestra cómo su Eiffel puede ser usado en todas las fases del desarrollo. Claro, Eiffel es un lenguaje que se presta especialmente para este uso.Los gráficos, en todo caso, tienen sentido, pero como método de apoyo, para cristalizar (por decirlo de algún modo) ideas e intuiciones... y sobre todo, como medio de comunicación con posibles expertos en el sistema modelado que no necesariamente conozcan lo suficiente de Informática. Cuando hay que sufrir un jefe de proyecto que no tiene ni zorra idea de programación, es preferible "obligar" al individuo para que use UML con nosotros, para intentar que lo que diga tenga un mínimo de estructuración y formalismo.
  • Los gráficos, en general, y UML en particular, tienen un "nosequé" que lo hacen más potente que cualquier formalismo basado en texto.
Eso puede ser verdad en el kindergarten, e incluso tengo serias dudas sobre si los críos entienden mejor un dibujo que una explicación verbal. ¡Por favor!, hay que recordar que la Civilización llegó cuando la gente dejó de dibujar bisontes en las paredes de la cueva y aprendió a escribir cosas como: "Churri, no te olvides de pasar por la carnicería".
Ocurre, además, que UML, como formalismo, cojea. Preguntas retóricas para una calurosa tarde de mayo:
  • ¿Cuántos tipos de gráficos ofrece UML?
  • ¿Cuántos de estos tipos suele usted ver en libros de Informática (excepto en los que se dedican a explicar el propio UML)?
  • ¿Cuántos de ellos son verdaderamente útiles?
Interfaces en Visual Studio 2005¿Verdaderamente útiles? Los diagramas de clases. Sin embargo, la gente suele confundirse con las cardinalidades de las relaciones, o se centra exclusivamente en la herencia. De todos modos, un diagrama de clases es útil, aunque al final... cuando se trate de bases de datos, la gente termine prefiriendo los clásicos diagramas de "patas de gallo".
¿Qué otro tipo de diagrama ofrece UML que realmente merezca la pena usar? Un servidor suele incluir algunos diagramas de secuencia en libros y cursos. ¿Sabe por qué lo hago? ¡Porque son tan raros y complicados que estoy seguro de que, cuando el lector se rompe la cabeza para descifrarlos, está repensando el contenido que ya he contado en el texto! Decían los antiguos romanos que, quien escribe, lee dos veces. Digo yo que, quien escudriña un diagrama de secuencias, piensa en el problema subyacente unas cuarenta veces más.
Seamos sinceros: ¿qué entiende usted mejor? ¿Cuatro líneas de pseudo código, o un intrincado diagrama de secuencias?
Otros tipos de gráficos, de tan triviales, terminan por parecer infantiles. Estoy pensando en los "use cases": un muñeco hecho con palotes y óvalos con textos encerrados. Los diagramas de estados tienen un poco más de sentido, pero esta vez, las reglas y convenios de UML son tan enrevesadas, y los propios gráficos tan parecidos a otros gráficos de UML, que al final la gente termina por dibujarlos como le da la gana, y guiándose por la dirección desde donde sopla el viento.
Hay otro problema importantísimo: UML fue diseñado por gente que usaba C++ como lenguaje de programación. C++ es muy potente, muy eficiente... pero su expresividad y claridad, sinceramente, dejan mucho que desear. Luego esa misma gente se aficionó a Java. Cuando alguien que trabaja en C# (¡o incluso en Delphi!) se enzarza con un diagrama UML en la fase de diseño, descubre importantes carencias. ¿Cómo se representa un evento en un diagrama de secuencias, por ejemplo? Claro, se puede alegar que UML no debería trabajar en ese nivel de detalles. Bravo. Estupendo. Pero entonces, ¿qué utilidad me puede proporcionar en la fase de diseño, una vez que el jefe lego en programación se ha encerrado en su despacho a jugar al Solitario?
Seguramente a esta explicación le faltan argumentos. Lo bueno de una bitácora es que no te obliga a vaciarte los bolsillos en un único mensaje. Me he centrado en UML para simplificar... pero algo parecido, y en ocasiones incluso peor, ocurre en otras áreas del mundillo de la "ingeniería de software".
Por ejemplo, la mayoría del contenido de la nueva moda, eso que llaman extreme programming, método Agile, etc, etc, es, o reglas triviales que nada aportan como novedad... o simplemente disparates. A mí se me cayó el alma al suelo hace un par de años cuando vi que Borland comenzaba a publicar en la página de su community sesudos artículos sobre la conveniencia... ¡de usar colores en los diagramas de software! Otra moda: la idea de la test-driven programming. Esta vez, hay un núcleo importante de verdad detrás del método. Pero para que la gente compre libros y software, y para que sus propagadores ganen dinero, el núcleo verdadero de la metodología test driven (que tiene mucha relación con la programación por contrato, la verificabilidad, etc, etc) se ha recubierto con capas y capas de fundamentalismo y tontería. Hay aplicaciones que admiten la metodología test driven, es cierto. También es cierto que en otros tipos de aplicaciones, es un disparate plantear que el desarrollo vaya guiado por pruebas sobre las especificaciones.
¿Un ejemplo? Intente escribir un ray tracer o un compilador con esta metodología. No avanzaría, se lo aseguro. ¿Por qué? Porque en el fondo, el método test-driven exige pequeños cambios incrementales en la funcionalidad y estructura del código. Sin embargo, los dos modelos de aplicaciones que he mencionado exigen el correcto funcionamiento de subsistemas realmente grandes antes de que usted pueda conectar algo y que se puedan hacer pruebas. Incluso después de ese momento, hay ciertos cambios que exigen que el programador tenga la mayor parte del modelo teórico en su cabeza para que la cosa funcione. ¡Esto es trivial, por Júpiter! Pero quien te cuenta las virtudes del test driving nunca te lo cuenta.
La programación tiene también su lado místico...... y, sin embargo, de las cosas que realmente tiene que conocer el programador, pocas veces se habla. ¿Sabía usted que es más difícil comprender qué hace una aplicación orientada a objetos a partir de su código fuente, que comprender el funcionamiento de una aplicación basada en la vieja programación estructurada? ¿No me cree? Mírelo de este modo: la metodología object oriented consiste en realizar la división modular utilizando la clasificación de entidades como principal criterio, mientras que antes se primaban los criterios funcionales. Cuando usted abre el código de XSight RT, por ejemplo, se encuentra con una clase Sphere que "ofrece" los métodos y propiedades tales y cuales. Vale, ¿y quién usa estos métodos y propiedades? ¿Cómo empieza a rodar la bola que debe luego provocar la avalancha? ¡Ah, amigo mío, para conectar los puntos estará obligado a comprobar detenidamente el funcionamiento de la jerarquía de clases basada en BaseSampler, y seguir las vías secundarias que conectan los samplers con el código asociado a escenas y cámaras. Por el contrario, si XSight RT fuese una aplicación con descomposición modular funcional, habría un método Main muy claro y visible, y usted podría entender qué hace la aplicación en un plis plas. Es un trozo de conocimiento clásico: mientras que la programación estructura se centraba en el cómo, la POO enfoca el quién. Y no se puede estar en misa y repicando a la vez. ¿Se lo ha escuchado decir a alguien? Sin embargo, es de sentido común...

Sé que éste es un tema polémico. Llevo quince minutos tecleado como un poseso, y todavía me quedan explicaciones y ejemplos. Es interesante analizar por qué existe un mercado para este tipo de cosas: para las "metodologías de salvación del alma del programador" y para los remedios homeopáticos (buena comparación, por cierto). Voy a dejarlo aquí por hoy. Creo que es un debate interesante, y os animo a que os suméis y deis vuestra opinión.

miércoles, mayo 17, 2006

Gestores de proyectos

¿Qué software de gestión de proyectos utiliza usted... si es que utiliza alguno? Me han pedido consejo sobre este asunto, pero lo cierto es que ni siquiera tengo claro cuál es el nombre genérico de este tipo de utilidades: "control de versiones", que es el otro nombre que recuerdo, me parece muy limitado. Con esto no estoy desaconsejando el uso de estos gestores. Simplemente, que nunca he tenido necesidad de usarlos: he trabajado en aplicaciones muy grandes, pero con equipos muy reducidos. Mi teoría, además, es que nunca dos personas tendrían que tocar el mismo fichero de código: cuando esto se produce, hay probablemente un fallo en el diseño de los módulos del proyecto. Además, detesto de todo corazón el modelo de cooperación que llamo "piloto/copiloto": un tío se sienta a teclear mientras el otro mira y comenta las "jugadas" como si fuese un partido de futbol. Y por desgracia, en esto es en lo que terminan muchos de los casos de desarrollo en equipo. Pero repito: es una opinión muy personal y subjetiva, y si os fijáis, ni siquiera es algo que haya puesto nunca en blanco y negro en un libro.

¿Con qué trabajáis? ¿Qué recomendáis? Para esta persona, es importante que el gestor sea compatible con Delphi y Visual Studio, de ser posible, aunque me parece que su uso principal, de momento, será con Delphi. Por experiencia, sé que muchas empresas usan el viejo Visual Source Safe con Delphi, pero no sé qué tal estará el problema de compatibilidad a estas alturas.

Si tenéis bitácora propia y queréis contestar con un artículo, enviadme la URL y os enlazo desde este mismo tema. Y por supuesto, también podéis hacerlo en los comentarios. En todo caso, muchas gracias, por adelantado...

miércoles, mayo 10, 2006

Intento fallido

Después de haber publicado la entrada anterior en la bitácora (En busca del tiempo perdido) y el correspondiente truco en mi página (Hell will freeze), me di cuenta de que había una posibilidad de transformar llamadas virtuales en llamadas "normales", sin necesidad de generar o modificar código a la medida... y sin necesidad de provocar una explosión combinatoria en el número de clases necesarias. Por supuesto, me sentí como un idiota por haber publicado el planteamiento de un problema aparentemente abierto sin percatarme de que existía una solución casi trivial.
Pero no cante aún victoria, amigo. He hecho un ensayo, para comprobar que la solución realmente funcionaba y... ¡fiasco!: el entorno de ejecución no parece efectuar la optimización necesaria. He mirado el código IL, el código nativo generado por el compilador just in time, y para estar completamente seguro, he medido los tiempos de ejecución.
De todos modos, creo que es bueno que le cuente en qué consistía la cura maravillosa que no funciona. Es un ejemplo, a pesar de todo, del tipo de oportunidades que ofrecen los tipos genéricos de .NET... porque mi solución consistía en utilizar, precisamente, genericidad.
XSight RT utiliza continuamente una interfaz llamada IShape. Voy a mostrarle solamente un método de ella, y voy a omitir parámetros y valor de retorno para simplificar la explicación:
public interface IShape
{
void ShadowTest();
}
Todas las figuras básicas (esferas, cubos, cilindros) se representan como clases que implementan IShape. Estas clases siempre se declaran "selladas" (sealed), para que al impedir derivar nuevas clases a partir de ellas, los compiladores puedan aplicar determinadas optimizaciones.
Hay más clases que implementan IShape. Tenemos, por ejemplo, las transformaciones euclideanas clásicas, como la rotación. La clase Rotate, en realidad, usa IShape por partida doble. Primero, implementa esta interfaz. Además, Rotate debe indicar cuál es el objeto que debe rotar. Naturalmente, esto se implementa mediante una variable llamada original, declarada con el tipo IShape:
public sealed class Rotate : IShape
{
private IShape original;
// ...
void IShape.ShadowTest() {
// Se rota un rayo en sentido inverso...
// y se llama a ShadowTest sobre "original".
original.ShadowTest();
}
}
La llamada al método ShadowTest de original es la que me gustaría optimizar. Es necesaria en el caso general, pero cuando se sabe a ciencia cierta que la escena está formada por una rotación que se aplica a un cubo, se podría sustituir la llamada virtual por una llamada a una ubicación fija en el segmento de código.
Mi idea consistía en convertir Rotate en una clase genérica:
public sealed class Rotate<T> : IShape
where T: class, IShape
{
private T original; // ¡ATENCION!
// ...
void IShape.ShadowTest() {
// ...
original.ShadowTest();
}
}
Para el compilador de C#, la variable original, aunque ahora su tipo es parámetro de tipo T, puede seguir apuntando alegremente a una esfera, a un cubo, o a cualquier clase que implemente IShape. Por lo tanto, cuando se genera código IL, la llamada conflictiva a ShadowTest sigue siendo una llamada virtual.
Ahora bien, el compilador JIT, es decir, el que traduce a código nativo al ejecutar, tiene más información a su alcance, y puede mejorar el código final que se ejecutará. Volvamos a la escena del cubo rotado. El compilador JIT tendrá que encargarse de instanciar el tipo genérico Rotate<T> y generar el tipo cerrado Rotate<Cube>. En esta clase cerrada generada en ejecución por .NET, la variable original es ahora de tipo Cube... ¡y esta es una clase sellada! El compilador JIT podría optimizar la llamada a ShadowTest porque ahora sabe a ciencia cierta que el código que se ejecutará es la versión de ShadowTest implementada por la clase Cube.
... pero el compilador JIT no optimiza esa llamada. Es lo que acabo de comprobar experimentalmente. No veo motivo alguno por el que no se pueda proceder así, e imagino que en alguna futura versión de .NET, el JITter podrá llegar a estas profundidades. De momento, todo indica que no lo hace.
He aprendido algo más gracias al experimento. En XSight RT, la funcionalidad básica de las figuras geométricas se expresa mediante un tipo de interfaz, IShape. Pero también podría haber definido los métodos y propiedades comunes por medio de una clase base abstracta, de la que deberían descender directa o indirectamente todas las demas clases de figuras. Lo que he averiguado, sin embargo, es que las llamadas a estos métodos comunes virtuales son más lentas cuando se usa la técnica de la clase base abstracta que cuando se utiliza un tipo de interfaz. En realidad, ¡son casi tres veces más lentas! La verdad es que me ha sorprendido tanta diferencia, por lo que pienso seguir investigando este misterio.

Etiquetas: ,

lunes, mayo 08, 2006

En busca del tiempo perdido

¿Y si le cuento que, después de cerrar la versión de XSight RT incluida con los cursos para C#, he logrado reducir el tiempo de generación de imágenes a menos de la mitad, en la mayoría de las escenas?
El "logro" se debe a un optimizador de escenas que he añadido... y a haber jugado un poco con el código generado, ya no por el compilador, sino por el JITter, el traductor a código nativo. Descubrí que ciertas propiedades de tipo estructura no estaban siendo expandidas inline, y al corregirlo, he logrado un aumento importante de la velocidad.
Como subproducto, se me han ocurrido un par de ideas para resolver problemas causados por la Programación Orientada a Objetos. No se extrañe: incluso las medicinas tienen efectos secundarios indeseables. La terapia que propongo funcionaría en un ray tracer, pero no estoy seguro de que pueda aplicarse a otros tipos de proyectos. De todos modos, la explico en un nuevo truco: