Compartir a través de


CLR

Información general de las mejoras de rendimiento en .NET 4.5

Ashwin Kamath

 

Este artículo analiza la versión preliminar de Microsoft .NET Framework 4.5. Toda la información relacionada está sujeta a cambios.

En el equipo de Microsoft .NET Framework, siempre supimos que mejorar el rendimiento es tan valioso para los desarrolladores como agregar nuevas características de tiempo de ejecución y API de biblioteca. El .NET Framework 4.5 incluye importantes inversiones en el rendimiento, lo que beneficia todos los escenarios de aplicaciones. Además, ya que .NET 4.5 es una actualización de .NET 4, incluso las aplicaciones .NET 4 pueden aprovechar muchas de las mejoras de rendimiento de las características existentes de .NET 4.

A la hora de permitirle a los desarrolladores proporcionar experiencias de aplicación satisfactorias, el tiempo de inicio (consulte msdn.microsoft.com/magazine/cc337892), el uso de memoria (consulte msdn.microsoft.com/magazine/dd882521), el rendimiento y la capacidad de respuesta realmente importan. Establecemos metas para el mejoramiento de estas métricas para los distintos escenarios de aplicación y, a continuación, diseñamos los cambios necesarios para cumplirlas o superarlas. En este artículo, entregaré información general de alto nivel sobre algunas de las mejoras de rendimiento clave que le realizamos a .NET Framework 4.5.

CLR

En esta entrega, nos centramos en: explotar los núcleos de múltiples procesadores para mejorar el rendimiento, reducir la latencia en el recolector de elementos no utilizados y mejorar la calidad del código de las imágenes nativas. A continuación aparecen algunas de las características clave de las mejoras de rendimiento.

Varios núcleos justo a tiempo (JIT). Vigilamos continuamente los avances de hardware de nivel bajo y trabajamos con proveedores de chip para alcanzar el mejor rendimiento asistido por hardware. En especial, hemos contado con chips de varios núcleos en nuestros laboratorios de rendimiento desde su aparición y hemos realizado los cambios apropiados para explotar dicho cambio en hardware. Sin embargo, esos cambios solo beneficiaron a algunos clientes en un principio.

En este momento, casi todos los equipos poseen, por lo menos, dos núcleos, de manera que las nuevas características que requieren más de un núcleo se usan ampliamente de manera inmediata. En las primeras etapas de desarrollo de .NET 4.5, quisimos determinar si resultaba razonable usar núcleos de procesadores múltiples para compartir la tarea de compilación justo a tiempo (específicamente como parte del inicio de la aplicación) para acelerar la experiencia general. Como parte de dicha investigación, descubrimos que una cantidad suficiente de aplicaciones administradas posee un número límite mínimo de métodos compilados justo a tiempo como para que la inversión valiera la pena.

La característica funciona a través de métodos de compilación justo a tiempo que posiblemente se ejecutan en un subproceso en segundo plano, que en una máquina de varios núcleos se ejecuta en otro núcleo, de forma paralela. En un caso ideal, el segundo núcleo se adelante rápidamente a la ejecución de línea principal de la aplicación, por lo que la mayoría de los métodos ya están compilados justo a tiempo para cuando se les necesita. Para poder saber qué métodos se debe compilar, la característica genera datos de perfil que hacer un seguimiento de los métodos que se ejecutan y, a continuación, se guía por dichos datos de perfil en una ejecución posterior. Este requerimiento de generación de datos de perfil es la forma principal en la que uno interactúa con la característica.

Con una pequeña adición de código, se puede usar esta característica del tiempo de ejecución para mejorar ostensiblemente los tiempos de inicio de las aplicaciones del cliente y de los sitios web. En especial, se deben realizar llamados directos a dos métodos estáticos en la clase ProfileOptimization, en el espacio de nombres System.Runtime. Consulte la documentación de MSDN para obtener más información. Observe que esta característica está habilitada de forma predeterminada para las aplicaciones ASP.NET 4.5 y las aplicaciones Silverlight 5.

Imágenes nativas optimizadas para varios lanzamientos. Lo hemos habilitado para que compilar previamente códigos para imágenes nativas a través de una herramienta llamada Generación de imágenes nativas (NGen). Las imágenes nativas que se originan a partir de esto tienen como resultado común un inicio más rápido de la aplicación que el que se ve con la compilación justo a tiempo. En este lanzamiento presentamos una herramienta complementaria llamada Optimización administrada guiada por perfiles (MPGO), la cual optimiza la distribución de las imágenes nativas para alcanzar un rendimiento incluso mejor. MPGO usa una tecnología de optimización guiada por perfiles, un concepto muy parecido a los varios núcleos justo a tiempo descritos anteriormente. Los datos de perfil para la aplicación incluyen un escenario representativo o un conjunto de escenarios, que se pueden usar para reordenar la distribución de una imagen nativa, de forma que dicho método, junto con otras estructuras de datos necesarias para el inicio, se ubiquen de forma densa dentro de una parte de la imagen nativa, lo que tiene como resultado en un tiempo de inicio más corto y un conjunto de trabajo menor (el uso de memoria de una aplicación). De acuerdo con nuestras pruebas y experiencia, por lo general vemos una ventaja en MPGO para grandes aplicaciones administradas (por ejemplo, grandes aplicaciones de GUI interactivas) y recomendamos enfáticamente su uso para este tipo de uso.

La herramienta MPGO genera datos de perfil para un DLL de lenguaje intermedio (IL) y agrega el perfil como un recurso a DLL de IL. La herramienta NGen se usa para la compilación previa de DLL de IL después de la generación del perfil y realiza una optimización adicional debido a la presencia de los datos de perfil. La figura 1 visualiza el flujo del proceso.

Process Flow with the MPGO Tool
Figura 1 Flujo del proceso con la herramienta MPGO

Asignador de Montón de objetos grandes (LOH). Muchos desarrolladores de .NET pedían una solución para el problema de la fragmentación de LOH o una forma de forzar la compresión del LOH. Puede leer más acerca del funcionamiento de LOH en la columna de Todo sobre CLR de junio de 2008 escrita por Maoni Stephens en msdn.microsoft.com/magazine/cc534993. En resumen, cualquier objeto de 85.000 bytes o más se asigna al LOH. Actualmente, el LOH no está comprimido. Comprimir el LOH consumiría mucho tiempo ya que el recolector de elementos no utilizados tendría que mover objetos grandes y, por lo tanto, resulta ser una proposición costosa. Cuando se recolectan los objetos del LOH, liberan espacio entre los objetos que sobreviven a la recolección, lo que genera el problema de la fragmentación.

Para explicarlo con más detalles, el CLR crea una lista de los objetos inactivos, lo que permite que se puedan reutilizar más adelante para satisfacer las solicitudes de asignación de objetos grandes y los objetos inactivos adyacentes se convierten en un solo objeto libre. Finalmente, un programa puede verse en la situación en que estos fragmentos de memoria libre entre objetos activos no son lo suficientemente grandes como para hacer espacio para la asignación del objeto en el LOH y debido a que la compresión no es una opción, rápidamente se convierte en un problema. Esto conlleva que las aplicaciones no respondan y finalmente a excepciones de falta de memoria.

En .NET 4.5 realizamos algunos cambios para hacer un uso eficaz de los fragmentos de memoria en el LOH, especialmente con relación a la forma en que administramos la lista libre. Los cambios se aplican a la estación de trabajo y a la recolección de elementos inactivos (GC) del servidor. Tenga en cuenta que esto no cambia el límite de 85.000 bytes para los objetos de LOH.              

GC en segundo plano para servidores. En .NET 4 habilitamos la GC en segundo plano para la GC de la estación de trabajo. Desde entonces, hemos visto una frecuencia creciente de máquinas con tamaños de montones de alto nivel que van desde un par de gigabytes hasta decenas de gigabytes. Incluso un recolector paralelo optimizado, como el nuestro, puede tardar segundos en recolectar montones tan grandes, lo que termina por bloquear los subprocesos de la aplicación por varios segundos. GC en segundo plano para servidores presenta un respaldo para la recolección simultánea de nuestro recolector de servidor. Minimiza las largas recolecciones que originan bloqueos, mientras mantiene un alto rendimiento de la aplicación.

Si usa una GC de servidor, no necesita hacer nada para aprovechar esta nueva característica, ya que la GC en segundo plano del servidor se ejecutará automáticamente. Las características de la GC en segundo plano de alto nivel son las mismas para la GC de cliente y de servidor:

  • Solo la GC total (generación 2) puede ocurrir en segundo plano.
  • La GC en segundo plano no comprime.
  • La GC en primer plano (GC de generación 0/generación 1) puede ocurrir durante la GC en segundo plano. La GC de servidor se realiza en subprocesos dedicados de la GC del servidor.
  • La GC completamente bloqueante también sucede en subprocesos dedicados de la GC del servidor.

Programación asincrónica

Se presentó un nuevo modelo de programación asincrónica, como parte del CTP asincrónico de Visual Studio y ahora es una parte importante de .NET 4.5. Estas nuevas características de lenguaje en .NET 4.5 le permiten escribir códigos asincrónicos de manera productiva. Dos nuevas palabras clave de lenguaje en C# y Visual Basic, llamadas "async" y "await" permiten este nuevo modelo. También se actualizó .NET 4.5 para que sea compatible con aplicaciones asincrónicas que usan estas nuevas palabras clave.

El Portal de programación asincrónica de Visual Studio en MSDN (msdn.microsoft.com/vstudio/async) es una gran fuente de ejemplos, informes técnicos y charlas sobre las nuevas características y de soporte técnico de este nuevo lenguaje.

Bibliotecas de procesamiento paralelo

Se realizaron varias mejoras a las bibliotecas de procesamiento paralelo (PCL) en .NET 4.5, con el fin de mejorar las API existentes.

Se optimizaron las clases Faster Lighter-Weight Tasks The System.Threading.Tasks.Task y Task<TResult> para usar menos memoria y para que se ejecuten más rápido en escenarios clave. Se vieron mejoras de hasta un 60% en casos relacionados con la creación de Tareas y la programación de continuaciones.

Más consultas PLINQ se ejecutan en paralelo. PLINQ recurre a la ejecución paralela cuando estima que será más dañino (hacer las cosas más lento) hacer una consulta de forma paralela. Estas decisiones son estimaciones bien intencionadas y no siempre son perfectas y en .NET 4.5, PLINQ reconocerá más clases de consultas de las que puede realizar de forma paralela correctamente.

Colecciones simultáneas más rápidas. Se realizaron ciertos ajustes a System.Collections.Concurrent.ConcurrentDictionary<TKey, TValue> para que fuera más rápido en ciertos escenarios.

Para obtener más información acerca de estos cambios, consulte el blog del equipo de la Plataforma de informática en paralelo en blogs.msdn.com/b/pfxteam.

ADO.NET

Compatibilidad de filas para la compresión de bits nulos. Los datos nulos son especialmente comunes para los clientes que aprovechan la característica de columnas dispersas de SQL Server 2008. Los clientes que aprovechan la característica de columnas dispersas puede producir conjuntos de resultados que contienen un gran número de columnas nulas. Para este caso, se presentó la compresión de filas de bits nulos (token SQLNBCROW o simplemente NBCROW) Esto reduce el espacio usado por las filas de conjuntos de resultados del servidor con grandes números de columnas, al comprimir columnas múltiples con valores NULL en una máscara de bits. Esto ayuda enormemente a la compresión de datos del protocolo de secuencia de datos tabular (TDS), cuando hay muchas columnas nulas en los datos.

Entity Framework

Consultas LINQ compiladas automáticamente. Cuando se escribe una consulta de LINQ to Entities actualmente, Entity Framework viaja a través del árbol de expresión generado por el compilador de C#/Visual Basic y la traduce (o compila) en SQL, como se muestra en la figura 2.

A LINQ to Entities Query Translated into SQL
Figura 2 Una consulta LINQ to Entities traducida a SQL

Sin embargo, compilar el árbol de expresión en SQL involucra cierta sobrecarga, especialmente para consultas más complejas. En versiones anteriores de Entity Framework, si se quería evitar tener que pagar por esta penalidad de rendimiento cada vez que se ejecutaba una consulta LINQ, se debía usar la clase CompiledQuery.

Esta nueva versión de Entity Framework es compatible con una nueva característica llamada Consultas LINQ compiladas automáticamente. Ahora cada consulta LINQ to Entities que se ejecuta automáticamente se compila y se ubica en la memoria caché de plan de consultas de Entity Framework. Cada vez adicional que se ejecute la consulta, Entity Framework la buscará en la memoria caché de consultas y no tendrá que pasar por todo el proceso de compilación nuevamente. Puede leer más acerca de esto en bit.ly/iCaM2b.

Windows Communication Foundation y Windows Workflow Foundation

El equipo de Windows Communication Foundation (WCF) y Windows Workflow Foundation (WF) también realizó un gran número de mejoras de rendimiento en este lanzamiento, como por ejemplo:

  • Mejoras en la escalabilidad de la activación TCP: Los clientes informaron un problema con la activación TCP, ya que cuando muchos usuarios simultáneos enviaban solicitudes con reconexiones constantes, el servicio de uso compartido de puertos TCP no escalaba bien Esto se corrigió en .NET 4.5.
  • Compatibilidad de compresión GZip integrada para WCF HTTP/TCP: Con esta nueva compresión, esperamos una tasa de compresión de 5x.
  • Reciclaje del host cuando el uso de memoria es alto para WCF: Cuando el uso de memoria es alto (perilla configurable) usamos la lógica usada menos recientemente (LRU) para reciclar los servicios WCF.
  • Compatibilidad con la transmisión asincrónica de HTTP para WCF: Implementamos esta característica en .NET 4.5 y logramos el mismo rendimiento que la transmisión sincrónica, pero con una escalabilidad mucho menor.
  • Mejoras de la fragmentación de generación 0 para WCF TCP.
  • Administrador del búfer optimizado para WCF para objetos grandes: Para objetos grandes, se implementó la agrupación de búfer para evitar los altos costos de la GC de generación 2.
  • Mejora de la validación WF con almacenamiento en caché de expresión: Esperamos mejoras de 3x para un escenario central de carga de WF y su ejecución.
  • Implementación de seguimiento de eventos integral de WCF/WF para Windows (ETW): Si bien no corresponde a una característica de mejora de rendimiento, sí ayuda a los clientes en las investigaciones de rendimiento.

Puede obtener más información en el blog del equipo de flujo de trabajo en blogs.msdn.com/b/workflowteam y en el artículo de MSDN Library en bit.ly/n5VCtU.

ASP.NET

Mejorar la densidad del sitio (también definido como "consumo de memoria por sitio") y el tiempo de inicio en frío de los sitios en el caso hospedaje compartido, han sido dos metas de rendimiento clave para el equipo de ASP.NET para .NET 4.5.

En casos de hospedaje compartido, muchos sitios comparten la misma máquina. En dichos casos, el tráfico suele ser bajo. Los datos proporcionados por algunas empresas de hospedaje muestran que la mayoría del tiempo la solicitud por segundo es inferior a 1 rps, con alzas ocasionales de 2 rps o más. Esto quiere decir que muchos procesos de trabajo probablemente morirán cuando están inactivas por un tiempo prolongado (20 minutos de forma predeterminada en IIS 7 y posterior). Por lo tanto, el tiempo de inicio se vuelve muy importante. En ASP.NET, ese corresponde al tiempo que le toma a un sitio web recibir una solicitud y responderla, cuando el proceso de trabajo estuvo caído, en comparación a cuando el sitio web ya estaba compilado.

Implementamos varias características en este lanzamiento para mejorar el tiempo de inicio para los escenarios de hospedaje compartido. Las características usados son:

  • Internamiento de ensamblados bin (compartir ensamblados comunes): La característica de instantánea de ASP.NET permite que se actualicen ensamblados que se usan en un dominio de aplicación sin descargar dicha AppDomain (necesario ya que el CLR bloquea los ensamblados en uso) Esto se realiza al copiar los ensamblados de la aplicación a una ubicación separada (ya sea una ubicación predeterminada determinada por CLR o una especificada por el usuario) y al cargar los ensamblados de dicha ubicación. Esto permite la actualización del ensamblado original mientras la instantánea está bloqueada. ASP.NET activa esta característica de forma predeterminada para los ensambles de carpetas bin, de manera que se pueda continuar con la actualización de los DLL mientras se ejecuta el sitio.
  • ASP.NET reconoce la carpeta bin del sitio web como una carpeta especial para ensamblados especiales (DLL) para controles personalizados de ASP.NET, componentes u otros códigos que se necesitan referenciar en una aplicación ASP.NET y compartir entre varias páginas de un sitio. Un ensamblado compilado en la carpeta bin se referencia automáticamente a lo largo de la aplicación web. ASP.NET también detecta la última versión de un DLL específico en la carpeta bin para su uso por el sitio web. Las aplicaciones preempaquetadas cuya intención es que se usen por los sitios ASP.NET, por lo general se instalan en la carpeta bin en vez de la memoria caché de ensamblados global.
  • Los equipos de ASP.NET y CLR descubrieron que cuando muchos sitios residen en el mismo servidor y usan la misma aplicación, muchas de las instantáneas de DLL tienden a ser exactamente las mismas. Ya que estos archivos se leen del disco y se cargan en la memoria, se originan muchas cargas redundantes que incrementan el tiempo de inicio y el consumo de memoria. Trabajamos en el uso de vínculos simbólicos que el CLR pueda seguir y luego implementamos la identificación de archivos comunes y los internamos en una ubicación especial (hacia donde apuntan los vínculos simbólicos). ASP.NET configura automáticamente las instantáneas en las que los DLL de bin deben estar. Los servicios de hospedaje compartido ahora pueden configurar sus equipos de acuerdo con las directrices de ASP.NET para obtener la mayor ventaja posible de rendimiento.
  • Varios núcleos justo a tiempo: Consulte la información relacionada en la sección previa de "CLR". El equipo de ASP.NET usa la característica de varios núcleos justo a tiempo para mejorar el tiempo de inicio al diseminar la compilación justo a tiempo a lo largo de los núcleos del procesador. Esto está activado de manera predeterminada en ASP.NET, por lo que puede aprovechar esta característica sin la necesidad de realizar ningún trabajo adicional. Puede desactivarlo con el uso de la siguiente configuración en el archivo web.config:
<configuration>
<!-- ... -->
<system.web>
<compilation profileGuidedOptimizations="None" />
<!-- ... -->
  • Captura previa: La tecnología de captura previa en Windows es muy eficaz a la hora de reducir el costo de lectura de disco de la paginación durante el inicio de la aplicación. La captura previa ahora también está habilitada en Windows Server (pero no de manera predeterminada). Para activar la captura previa para el hospedaje web de alta densidad, ejecute el siguiente conjunto de comandos en la línea de comandos:
sc config sysmain start=auto
reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PrefetchParameters" /v EnablePrefetcher /t REG_DWORD /d 2 /f
reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Prefetcher" /v MaxPrefetchFiles /t REG_DWORD /d 8192 /f
net start sysmain
  • A continuación, puede actualizar el archivo web.config para usarlo en ASP.NET:
<configuration>
<!-- ... -->
<system.web>
<compilation enablePrefetchOptimization
  ="true" />
<!-- ... -->
  • Ajustar la GC para el hospedaje web de alta densidad: La GC puede influir en el consumo de memoria del sitio, pero también se puede ajustar para permitir un mejor rendimiento. Se puede ajustar o configurar la GC para un mejor rendimiento de CPU (desacelerar la frecuencia de las recolecciones) o disminuir el consumo de memoria (es decir, recolecciones más frecuentes para liberar memoria antes). Para activar el ajuste de la GC, puede seleccionar la configuración HighDensityWebHosting en el archivo aspnet.config en la carpeta Windows\Microsoft\v4.0.30319 con el fin de conseguir un consumo menor de memoria (espacio de trabajo) por sitio:
<configuration>
<!-- ... -->
<runtime>
<performanceScenario
  value="HighDensityWebHosting" />
  <!-- ... -->

Puede obtener más información sobre las mejoras de rendimiento de ASP.NET en el informe técnico "Introducción a la nueva versión de ASP.NET" en bit.ly/A66I7R

Se reciben comentarios

La lista que aquí aparece no es exhaustiva. Existen más cambios menores para mejorar el rendimiento que no se tomaron en consideración para mantener el alcance de este artículo limitado a las características principales. Aparte de esto, los equipos de rendimiento de .NET Framework también han estado ocupados trabajando en mejoras de rendimiento específicas para las aplicaciones administradas estilo Metro de Windows 8. Cuando haya descargado y probado .NET Framework 4.5 y la versión beta de Visual Studio 11 para Windows 8, háganos llegar sus comentarios o sugerencias para los próximos lanzamientos.

Glosario de términos

Hospedaje compartido: También conocido como "hospedaje web compartido", el hospedaje web de alta densidad permite que cientos, o incluso miles, de sitios web se ejecuten en el mismo servidor. Al compartir los costos de hardware, se puede mantener cada sitio a un menor costo. Esta técnica ha disminuido considerablemente la barrera de entrada para los propietarios de sitios web.

Inicio en frío: El inicio en frío es el tiempo que le toma a la aplicación iniciarse cuando no está presente aun en la memoria. Puede experimentar el inicio en frío al iniciar una aplicación después de reiniciar el sistema. En el caso de aplicaciones grandes, el inicio en frío puede tomar varios segundos, ya que las páginas requeridas (código, datos estáticos, registro, etc.) no están presentes en la memoria y se requieren costosos accesos al disco para traer dichas páginas a la memoria.

Inicio en caliente: El inicio en caliente es el tiempo que le toma a la aplicación iniciarse cuando ya está presente en la memoria. Por ejemplo, si se inició una aplicación unos segundos antes, es probable que la mayoría de las páginas ya estén cargadas en la memoria y que el SO las reutilizará, lo que ahorra un costoso tiempo de acceso al disco. Es por eso que una aplicación se inicia mucho más rápido la segunda vez que la ejecuta (o por qué una segunda aplicación .NET se inicia más rápido que la primera, ya que partes de .NET ya están cargadas en la memoria).

Generación de imágenes nativas, o NGen: Se refiere al proceso de compilación previa de ejecutables de lenguaje intermedio (IL) en un código máquina antes del tiempo de ejecución. Esto tiene como resultado dos ventajas de rendimiento. En primer lugar, reduce el tiempo de inicio de la aplicación al evitar la necesidad de compilar códigos durante el tiempo de ejecución. En segundo lugar, mejora el uso de memoria al permitir que se compartan las páginas de código entre procesos múltiples. También existe una herramienta, NGen.exe, que crea imágenes nativas y las instala en la memoria caché de imagen nativa (NIC) o en el equipo local. El tiempo de ejecución carga las imágenes nativas cuando están disponibles.

Optimización guiada por perfiles: Se ha comprobado que la optimización guiada por perfiles mejora los tiempo de inicio y de ejecución de aplicaciones nativas y administradas. Windows proporciona el conjunto de herramientas y la infraestructura para realizar la optimización guiada por perfiles para ensamblados nativos, mientras que el CLR proporciona el conjunto de herramientas y la infraestructura para realizar las optimizaciones guiadas por perfiles para ensamblados administrados (llamada Optimización administrada guiada por perfiles, o MPGO). Muchos equipos usan estas tecnologías en Microsoft para mejorar el rendimiento de sus aplicaciones. Por ejemplo, el CLR realiza la optimización guiada por perfiles de ensamblados nativos (optimización guiada por perfiles de C++) y ensamblados administrados (con el uso de MPGO).

Recolector de elementos no utilizados: El tiempo de ejecución de .NET es compatible con la administración automática de memoria. Realiza un seguimiento de cada asignación de memoria hecha por el programa administrado y solicita el recolector de elementos no utilizados de forma periódica, el cual busca memoria que ya no se use y la reutiliza para nuevas asignaciones. Una optimización importante que realiza el recolector de elementos no utilizados es que no busca todo el montón cada vez, sino que divide el montón en tres generaciones (generación 0, generación 1 y generación 2). Para obtener más información sobre el recolector de elementos no utilizados, consulte la columna de Todo sobre CLR de junio de 2009 en msdn.microsoft.com/magazine/dd882521.

Compresión: En el contexto de la recolección de elementos no utilizados, cuando el montón alcanza cierto estado de fragmentación, el recolector de elementos no utilizados comprime el montón al acercar los objetos activos entre sí. El objetivo principal de comprimir el montón es crear bloques más grandes de memoria disponible en el que se puedan asignar más objetos.

Ashwin Kamath es jefe de programas en el equipo de CLR de .NET y estuvo a cargo de las características de rendimiento y de confianza de .NET Framework 4.5. Actualmente está trabajando en características de diagnóstico para la plataforma de desarrollo de Windows Phone.

Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Surupa Biswas, Eric Dettinger, Wenlong Dong, Layla Driscoll, Dave Hiniker, Piyush Joshi, Ashok Kamath, Richard Lander, Vance Morrison, Subramanian Ramaswamy, Jose Reyes, Danny Shih y Bill Wert