Este artigo foi traduzido por máquina.

CLR

Desenvolvimento do .NET para Processadores ARM

Andrew Pardoe

 

Os consumidores são um grande motor do mercado tecnologia hoje. Como evidenciado pela tendência conhecida como "o consumo de TI", bateria de longa duração e sempre conectado e mídia rica de experiências são importantes para todos os clientes de tecnologia. Para permitir a melhor experiência para aparelhos com bateria de longa duração, Microsoft está trazendo o Windows 8 OS sistemas construídos sobre o processador ARM de baixa potência, que alimenta a maioria dos dispositivos móveis hoje. Neste artigo, vou discutir detalhes sobre o Microsoft .net Framework e o processador do braço, o que você como um desenvolvedor de .net deve ter em mente e que nós na Microsoft (onde sou gerente de programa na equipe do CLR) tinha que fazer para trazer o .net sobre a braço.

Como um desenvolvedor de .net, você pode imaginar que escrever aplicativos para rodar em uma variedade de diferentes processadores colocaria um pouco de um dilema. Arquitetura do processador ARM (ISA) é incompatível com ISA do processador x86 Construído para executar nativamente em x86 de Apps funcionam bem em x 64 porque ISA do processador x64 é um superconjunto do x86 ISA Mas o mesmo não é verdadeiro de x86 nativo aplicativos em execução no braço — precisam ser recompilados para executar na arquitetura incompatível. Ser capaz de escolher entre uma variedade de dispositivos diferentes é ótimo para os consumidores, mas traz alguma complexidade para a história do desenvolvedor.

Escrevendo seu aplicativo em uma linguagem .net não só permite que você reutilize suas habilidades existentes e o código, ele também permite que seu aplicativo executar em todos os processadores do Windows 8 sem recompilação. Por portar o .net Framework para braço, ajudámos abstrair as características únicas da arquitetura que são desconhecidas para a maioria dos desenvolvedores do Windows. Mas ainda há algumas coisas que talvez você precise observar quando escrever código para executar no braço.

A estrada para o braço: .Net, passado e presente

O .net Framework já roda em processadores ARM, mas não é a exata mesma .net Framework como a versão executada na área de trabalho. Quando nós começamos a trabalhar na primeira versão do .net, percebemos que ser capaz de escrever código portátil facilmente em processadores foi a chave para nossa proposta de valor da produtividade do desenvolvedor de aumento. Processador x86 dominou o espaço de computação de desktop, mas uma enorme variedade de processadores existentes no espaço móvel e incorporado. Para permitir que desenvolvedores para os processadores de destino, nós criamos uma versão do .net Framework chamado o .net Compact Framework, que é executado em máquinas que possuem restrições de memória e processador.

Os primeiros dispositivos que suporte o .net Compact Framework tinham tão pouco quanto 4 MB de RAM e um CPU de 33 MHz. O design do .net Compact Framework enfatizou uma implementação eficiente (o que permitiu executar tais dispositivos restritos) e a portabilidade (para que ele pudesse correr em toda a vasta gama de processadores que eram comuns nos espaços móveis e incorporados). Mas os mais populares dispositivos móveis-smartphones — agora executados em configurações comparáveis nos computadores dos há 10 anos. A área de trabalho .net Framework foi projetado para rodar em máquinas com Windows XP pelo menos um processador de 300 MHz e 128 MB de RAM. Dispositivos de Windows Phone hoje exigem pelo menos 256 MB de RAM e um processador ARM Cortex moderno.

O .net Compact Framework é ainda uma grande parte da história do desenvolvedor do Windows Embedded Compact. Dispositivos embedded cenários executar configurações restrita, muitas vezes com menos de 32 MB de RAM. Também criamos uma versão do .net Framework chamado o .net Micro Framework, que roda em processadores que têm menos de 64 KB de RAM. Então na verdade, temos três versões do .net Framework, cada qual é executado em uma classe diferente de processador. Mas esta é a primeira vez que nosso principal produto, a área de trabalho .net Framework, juntou-se o compacto e Micro estruturas em rodando em processadores ARM.

Em execução no braço

Embora o .net Framework foi projetado para ser a plataforma neutra, ela é principalmente executada em hardware x86 ao longo de sua existência. Isso significa que alguns padrões de x 86 específicos tem escorregado na mente coletiva dos programadores de .net. Você deve ser capaz de se concentrar em escrever grandes apps em vez de escrever para a arquitetura do processador, mas você deve manter algumas coisas em mente ao escrever código .net para executar no braço. Estes incluem um modelo de memória mais fraco e mais rigorosos requisitos de alinhamento de dados, como também alguns lugares onde parâmetros da função são tratados de forma diferente. Finalmente, existem algumas etapas de configuração de projeto no Visual Studio que são diferentes quando você destino dispositivos. Falarei sobre cada um deles.

Um modelo mais fracos de memória um modelo de"memória" refere-se a visibilidade das alterações feitas no estado global em um programa multithread. Um programa que compartilha dados entre dois (ou mais) segmentos terá normalmente um bloqueio de dados compartilhados. Dependendo do bloqueio específico usado, se um thread está acessando os dados, outros segmentos que tentarem acessar os dados serão bloqueado até que o primeiro segmento é terminado com os dados compartilhados. Mas os bloqueios não são necessários se você sabe que cada segmento de acessar os dados compartilhados irá fazê-lo sem interferir com a visão dos outros segmentos de dados. Programação em tal maneira é chamada usando um algoritmo de "lock-free".

O problema com algoritmos sem bloqueio vem quando você não sabe a ordem exata em que seu código será executado. Processadores modernos reordenar as instruções para garantir que o processador pode progredir a cada ciclo de clock e combinar escreve a memória, a fim de diminuir a latência. Apesar de quase cada processador executa essas otimizações, há uma diferença em como a ordenação de leituras e gravações é apresentada para o programa. x garantia de processadores x86 que o processador vai olhar como ele está em execução a maioria lê e escreve na mesma ordem em que o programa especifica-los. Esta garantia é chamada de uma ordem de modelo, ou forte de gravação de memória forte. Processadores ARM não fazem tantas garantias — eles estão geralmente livres para se mover operações enquanto fazendo assim não muda a maneira que o código funcionaria em um programa de single-threaded. O processador ARM fazer algumas garantias que permitem cuidadosamente construído sem bloqueio código, mas tem o que é chamado um modelo de memória fraca.

Curiosamente, o próprio .net Framework CLR tem um modelo de memória fraca. Todas as referências para escrever a ordenação na especificação ECMA Common Language Infrastructure (CLI) (disponível como um PDF em bit.ly/1Hv1xw), o padrão que o CLR é projetado para atender, referir-se a acessos de voláteis. Em c#, isso significa acessos a variáveis marcadas com a palavra-chave volátil (ver secção 12,6 da especificação do CLI para referência). Mas na última década, tem sido executado mais código gerenciado em x86 sistemas e o compilador just-in-time (JIT) de CLR não tenha adicionado muito a reorderings permitida pelo hardware, então havia relativamente poucos casos onde o modelo de memória revelaria bugs simultâneos latente. Isso poderia representar um problema se gerenciado código testadas somente em x e escritas para máquinas x86 é esperado para trabalhar da mesma forma nos sistemas de braço.

A maioria dos padrões que exigem cuidado adicional no que diz respeito a reordenação são raras em código gerenciado. Mas alguns desses padrões que existem são enganosamente simples. Aqui está um exemplo de código que não olha como ele tem um bug, mas se essas estatísticas são alteradas em outro thread, esse código pode quebrar em uma máquina com um modelo de memória fraca:

static bool isInitialized = false;
static SomeValueType myValue;
if (!isInitialized)
{
  myValue = new SomeValueType();
  isInitialized = true;
}
myValue.DoSomething();

Para tornar este código correto, simplesmente indica que o sinalizador isInitialized é volátil:

static volatile bool isInitialized = false; // Properly marked as volatile

Execução desse código sem reordenação é mostrada no bloco de esquerda em Figura 1. 0 É o primeiro inicializar SomeValueType na pilha local e copia o SomeValueType criado localmente para uma AppDomain de localização global. Segmento 1 determina verificando isInitialized que também precisa de criar SomeValueType. Mas não há nenhum problema, porque os dados estão sendo gravados volta para o mesmo local global de AppDomain. (Maioria das vezes, como neste exemplo, qualquer mutações feitas pelo método DoSomething são idempotentes.)

Write Reordering
Figura 1 escrever a reordenação

O bloco do lado direito mostra a execução do código, mesmo com um sistema que ofereça suporte a reordenação de gravação (e uma tenda convenientemente colocada em execução). Este código não executar corretamente porque o Thread 1 determina lendo valor do isInitialized que SomeValueType não precisa ser inicializado. A chamada para DoSomething refere-se à memória que não foi inicializada. Quaisquer dados lidos alguns ValueType terá sido definidos pelo CLR para 0.

Seu código não vai executar frequentemente incorretamente devido a este tipo de reordenação, mas acontece. Estes reorderings são perfeitamente legais — não importa a ordem das gravações, quando em execução em um único segmento. É melhor quando se escreve código simultâneo para Marcar variáveis voláteis corretamente com a palavra-chave volátil.

O CLR é permitido expor um modelo de memória mais forte do que exige a especificação ECMA CLI. Em x86, por exemplo, o modelo de memória do CLR é forte porque o modelo de memória do processador é forte. A equipe do .net poderia ter feito o modelo de memória no braço tão forte como o modelo em x 86, mas garantindo a perfeita ordem sempre que possível pode ter um impacto notável no desempenho de execução de código. Já fizemos trabalhos direcionados para fortalecer o modelo de memória no braço — especificamente, podemos inserir barreiras de memória em pontos-chave ao escrever para o heap gerenciado para garantir a segurança de tipo — mas nós fizemos só faça isso com um impacto mínimo na performance. A equipe passou por várias revisões de projeto com especialistas para certificar-se de que as técnicas aplicadas no braço CLR estavam corretas. Além disso, os benchmarks de desempenho mostram que o desempenho de execução de código do .net dimensiona o mesmo que o código C++ nativo quando comparado em x 86, x 64 e braço.

Se seu código depende de algoritmos sem bloqueio que dependem da implementação do x86 CLR (ao invés da especificação ECMA CLR), você vai querer adicionar a palavra-chave volátil a variáveis relevantes, conforme o caso. Uma vez que você marcou o estado compartilhado como volátil, o CLR irá cuidar de tudo para você. Se você for como a maioria dos desenvolvedores, você está pronto para ser executado no braço porque já usados bloqueios para proteger seus dados compartilhados, corretamente marcado variáveis voláteis e testei o app no braço.

Dados alinhamento requisitos outra diferença que pode afetar alguns programas é que processadores ARM requerem alguns dados a serem alinhados. O padrão específico em que se aplicam os requisitos de alinhamento é quando você tem um valor de 64 bits (ou seja, um int64, um uint64 ou um casal) que não está alinhado em um limite de 64 bits. O CLR cuida de alinhamento para você, mas há duas maneiras para forçar um tipo de dados desalinhados. A primeira maneira é explicitamente especificar o layout de uma estrutura com o atributo personalizado [ExplicitLayout]. A segunda maneira é incorretamente especificar o layout de uma estrutura passada entre código gerenciado e nativo.

Se você notar uma chamada P/Invoke voltar com lixo, você pode querer dar uma olhada em todas as estruturas sendo empacotado. Como exemplo, temos corrigido um bug ao portar algumas bibliotecas .net, em que uma interface COM passado uma estrutura POINTL que contém dois campos de 32 bits para uma função no código gerenciado que tomou um 64-bit duplo como um parâmetro. A função usada bit operações para obter os dois campos de 32 bits. Aqui é uma versão simplificada da função buggy:

void CalledFromNative(int parameter, long point)
{
  // Unpack native POINTL from long point
  int x = (int)(point & 0xFFFFFFFF);
  int y = (int)((point >> 32) & 0xFFFFFFFF);
  ...  // Do something with POINTL here
}

O código nativo não tem que alinhar a estrutura POINTL em um limite de 64 bits, porque continha dois campos de 32 bits. Mas o braço requer o dobro de 64 bits para ser alinhado quando ele é passado para a função gerenciada. Certificando-se de que os tipos são especificados ser o mesmo em ambos os lados da chamada do código gerenciado e nativo é fundamental se seus tipos exigem alinhamento.

Dentro Visual Studio mais desenvolvedores já não vai notar as diferenças que eu discuti porque código .net por design não é específico para qualquer arquitetura de processador. Mas existem algumas diferenças no Visual Studio quando o perfil ou a depuração de aplicativos do Windows 8 em um dispositivo de braço, porque o Visual Studio não é executado em dispositivos de braço.

Você já está familiarizado com o processo de desenvolvimento de plataforma cruzada, se você escrever aplicações para Windows Phone. O Visual Studio executa na sua caixa de dev 86 x e você iniciar seu aplicativo remotamente no aparelho ou um emulador. O aplicativo usa um proxy instalado no seu dispositivo para se comunicar com a sua máquina de desenvolvimento através de uma conexão IP. Além da configuração inicial etapas, depuração e Perfil de experiências se comportam da mesma em todos os processadores.

Um outro ponto para estar ciente é que as configurações de projeto do Visual Studio Adicionar braço para x 86 e x 64 como uma opção de processador de destino. Normalmente, você vai escolher para AnyCPU de destino quando você escreve um aplicativo .net para Windows no braço, e seu aplicativo será executado apenas em todas as arquiteturas de Windows 8.

Indo profundamente no braço de apoio

Tendo feito agora no artigo, você já sabe muito sobre o braço. Agora eu gostaria de compartilhar alguns detalhes interessantes, técnicos profundo sobre o trabalho da equipe .net para apoiar o braço. Você não terá que fazer este tipo de trabalho em seu código .net — isto é apenas uma espiada por trás das cenas no tipo de trabalho que fizemos.

A maioria das mudanças dentro do CLR em si eram simples porque o CLR foi projetado para ser portátil entre arquiteturas. Nós tivemos que fazer algumas mudanças para se conformar ao braço Application Binary Interface (ABI). Nós também tivemos que reescrever o código de assembly do CLR para o destino a braço e alterar nosso compilador JIT para emitir instruções ARM Thumb 2.

A ABI especifica como a interface programável do processador. É semelhante à API que especifica o que, por meio de programação disponíveis funções de um sistema operacional. As três áreas do ABI que afetou nosso trabalho são a função chamando convenção, as convenções de cadastro e informações de desenrolamento de pilha de chamada. Vamos discutir cada uma delas.

A Convenção de chamada de função A Convenção de chamada é um acordo entre o código que chama uma função e a função que está sendo chamado. A convenção especifica como parâmetros e valores de retorno são dispostas na memória, bem como o que registra precisa ter seus valores preservados entre a chamada. Para função chamadas a trabalhar em limites (por exemplo, uma chamada a partir de um programa no sistema operacional), geradores de código necessário gerar as chamadas de função que coincide com a Convenção que define o processador, incluindo alinhamento de valores de 64 bits.

BRAÇO foi o primeiro processador de 32 bits onde o CLR teve que lidar com a alinhar objetos no heap gerenciado em um limite de 64 bits e parâmetros. A solução mais simples seria alinhar todos os parâmetros, mas a ABI exige que um gerador de código não deixar bolhas na pilha quando não há alinhamento é necessário, então não há nenhuma degradação de desempenho. Assim, a simple operação de empurrar um monte de parâmetros na pilha torna-se mais delicada no processador ARM. Como uma estrutura de usuário pode conter um int64, solução do CLR foi usar um pouco em cada tipo, para indicar se ele requer alinhamento. Isto dá o CLR informação suficiente para assegurar que as chamadas de função que contém valores de 64 bits não acidentalmente corromper a pilha de chamadas.

Registre Convenção A exigência de alinhamento de dados transporta mais quando estruturas são totalmente ou parcialmente registradas no braço. Isto significa que tivemos que modificar o código dentro do CLR que move freqüentemente usado dados da memória em registos para se certificar de que os dados estão corretamente alinhados nos registos. Este trabalho tinha que ser feito para duas situações: em primeiro lugar, certificando-se que valores de 64 bits iniciar nos registos do mesmo e segundo, colocando agregados homogêneos de ponto flutuante (HFAs) nos registos adequados.

Se um gerador de código registra um int64 no braço, deve ser armazenado em um par-ímpar par — ou seja, R0 R1 ou R2-R3. O protocolo para HFAs permite que até quatro valores de ponto flutuante de duplos ou simples em uma estrutura homogênea. Se estas estiverem registradas, devem ser armazenados em ambos o S (single) ou D (Duplo) registrar conjuntos, mas não no uso geral r registra.

Informações de informações desenrolar de desenrolamento registram os efeitos que uma chamada de função na pilha e registros onde registra a não-volátil são salvos através de chamadas de função. Em x 86, Windows olha FS: 0 para exibir uma lista vinculada de informações de registro de exceção de cada função no caso de uma exceção não tratada. 64-bit Windows introduziu o conceito de desenrolar informações que permite que o Windows Rastrear de pilha em caso de uma exceção não tratada. O design do braço estendido este desenrolar informações de projetos de 64 bits. Os geradores de código CLR, por sua vez, tinham que mudar para acomodar o novo design.

Assembly código mesmo que a maior parte do mecanismo de tempo de execução CLR é escrito em C++, temos o código de assembly que deve ser portado para cada novo processador. A maioria deste código assembly é o que chamamos de "funções de stub", ou "stubs". Categoria: serve como cola de interface que permite unir o C++ e compilado em JIT porções do tempo de execução. O restante do código assembly dentro do CLR é escrito em assembly para o desempenho. Por exemplo, a barreira de gravação coletor de lixo deve ser extremamente rápido, porque ele é chamado com freqüência — qualquer tempo uma referência de objeto é escrita em um objeto no heap gerenciado.

Um exemplo de um esboço é o que chamamos da "conversão de shuffle". Chamamos uma conversão de shuffle porque ele embaralha os valores de parâmetro através de registros. Às vezes, o CLR tem que alterar o posicionamento dos parâmetros nos registos antes de que é feita uma chamada de função. O CLR usa a conversão de shuffle para fazer isso quando chamar delegados.

Conceitualmente, quando você chamar um delegado, você apenas faz uma chamada para o método Invoke. Na realidade, o CLR faz uma chamada indireta através de um campo de representante ao invés de fazer um método nomeado chamar (exceto quando você chamar explicitamente chamar através de reflexão). Este método é muito mais rápido do que uma chamada de método nomeado porque o tempo de execução pode simplesmente trocar a instância do delegate (obtido o ponteiro de destino) para o delegado na chamada de função. Ou seja, para uma instância foo do delegado d, a chamada para o método d.Member é mapeada para o foo.Método de membro.

Se você fizer uma instância fechada delegado chamar, o ponteiro this é armazenado dentro o primeiro registro usado para passar parâmetros, R0, e o primeiro parâmetro é armazenado no próximo registro, R1. Mas isso só funciona quando você tem um delegado ligado a um método de instância. O que acontece se você estiver chamando um delegado estático aberto? Nesse caso, você espera que o primeiro parâmetro é armazenado em R0 (como não há nenhum este ponteiro.) A conversão de shuffle move o primeiro parâmetro de R1 para o segundo parâmetro em R0, R0 e assim por diante, como mostra Figura 2. Porque essa conversão de shuffle destina-se passar valores de registo para registo, ele precisa ser reescrito especificamente para cada processador.

A “Shuffle Thunk” Shuffles Value Parameters Across Registers
Figura 2 "Shuffle Thunk" embaralha parâmetros de valor através de registos

Foco apenas no código

Para revisar, portar o .net Framework para braço foi um projeto interessante e muito divertido para a equipe do .net. E escrever aplicativos .net para rodar em cima do .net Framework em braço deve ser divertido para você como um desenvolvedor .net. Seu código do .net pode executar diferentemente no braço do que em x processadores baseados em x86, em algumas situações, mas o ambiente de execução virtual do .net Framework abstrai normalmente essas diferenças para você. Isso significa que você não precisa se preocupar sobre qual arquitetura de processador seu aplicativo .net é executado no. Você pode se concentrar apenas em escrever código grande.

Acredito que ter o Windows 8 disponível no braço vai ser ótimo para desenvolvedores e usuários finais. Processadores ARM são especialmente adequados para bateria de longa duração, para que eles permitem que dispositivos de leves, portátil e sempre conectado. As questões mais significativas que você verá ao portar seu aplicativo para braço são as diferenças de desempenho de processadores para desktop. Mas certifique-se de executar o seu código no braço antes de dizer que realmente funciona no braço — não confia que o desenvolvimento em x86 é suficiente. Para a maioria dos desenvolvedores, que é tudo que é necessário. E se você tiver quaisquer problemas, você pode consultar este artigo para obter algumas dicas sobre onde começar a investigar.

Andrew Pardoe é gerente de programa da equipe de CLR, ajudando a enviar o Microsoft .net Framework em todos os tipos de processadores. Seu favorito pessoal permanece o Itanium. Ele pode ser contatado em Andrew.Pardoe@microsoft.com.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Brandon Bray, Layla Driscoll, Eric Eilebrecht e Rudi Martin