Ao projetar e implantar uma linguagem de programação orientada a objetos, em algum momento é preciso fazer uma escolha sobre a implementação de tipos fundamentais (como int
, float
, double
ou equivalentes) como classes ou outra coisa. Claramente, as linguagens da família C tendem a não defini-las como classes (Java possui tipos primitivos especiais, o C # as implementa como estruturas imutáveis, etc.).
Posso pensar em uma vantagem muito importante quando tipos fundamentais são implementados como classes (em um sistema de tipos com uma hierarquia unificada): esses tipos podem ser subtipos apropriados de Liskov do tipo raiz. Assim, evitamos complicar o idioma com boxe / unboxing (explícito ou implícito), tipos de wrapper, regras especiais de variação, comportamento especial etc.
Claro, eu posso entender parcialmente por que os designers de linguagem decidem da maneira que fazem: as instâncias de classe tendem a ter uma sobrecarga espacial (porque as instâncias podem conter uma tabela de dados ou outros metadados em seu layout de memória), que as primitivas / estruturas não precisam have (se o idioma não permitir herança nesses).
A eficiência espacial (e a localização espacial aprimorada, especialmente em matrizes grandes) são a única razão pela qual tipos fundamentais geralmente não são classes?
Geralmente, eu suponho que a resposta seja sim, mas os compiladores têm algoritmos de análise de escape e, portanto, podem deduzir se podem (seletivamente) omitir a sobrecarga espacial quando uma instância (qualquer instância, não apenas um tipo fundamental) é estritamente estrita. local.
O que foi dito acima está errado ou há algo que esteja faltando?
fonte
Respostas:
Sim, tudo se resume à eficiência. Mas você parece estar subestimando o impacto (ou superestimando o quão diversas otimizações funcionam).
Primeiro, não é apenas "sobrecarga espacial". Fazer primitivas encaixotadas / alocadas em heap também tem custos de desempenho. Há uma pressão adicional no GC para alocar e coletar esses objetos. Isso vale duplamente se os "objetos primitivos" são imutáveis, como deveriam ser. Depois, há mais falhas de cache (por causa da indireção e porque menos dados se encaixam em uma determinada quantidade de cache). Além disso, o simples fato de "carregar o endereço de um objeto e carregar o valor real a partir desse endereço" requer mais instruções do que "carregar o valor diretamente".
Segundo, a análise de escape não é um pó de fada mais rápido. Isso se aplica apenas a valores que, bem, não escapam. Certamente, é bom otimizar cálculos locais (como contadores de loop e resultados intermediários de cálculos) e isso trará benefícios mensuráveis. Mas uma maioria muito maior de valores vive nos campos de objetos e matrizes. É verdade que eles podem estar sujeitos à análise de escape, mas como geralmente são tipos de referência mutáveis, qualquer apelido deles representa um desafio significativo à análise de escape, que agora precisa provar que esses apelidos (1) também não escapam. , e (2) não fazem diferença com o objetivo de eliminar alocações.
Dado que chamar qualquer método (incluindo getters) ou passar um objeto como argumento para qualquer outro método pode ajudar o objeto a escapar, você precisará de uma análise interprocedural em todos os casos, exceto nos mais triviais. Isso é muito mais caro e complicado.
E há casos em que as coisas realmente escapam e não podem ser razoavelmente otimizadas. Muitos deles, na verdade, se você considerar com que freqüência os programadores C enfrentam o problema de alocar coisas de pilha. Quando um objeto que contém um int escapa, a análise de escape deixa de se aplicar ao int também. Diga adeus aos campos primitivos eficientes .
Isso se vincula a outro ponto: as análises e otimizações necessárias são seriamente complicadas e uma área ativa de pesquisa. É discutível se alguma implementação de linguagem alcançou o grau de otimização sugerido e, mesmo assim, tem sido um esforço raro e hercúlea. Certamente, ficar sobre os ombros desses gigantes é mais fácil do que ser você mesmo um gigante, mas ainda está longe de ser trivial. Não espere desempenho competitivo a qualquer momento nos primeiros anos, se é que alguma vez.
Isso não quer dizer que essas línguas não possam ser viáveis. Claramente eles são. Apenas não assuma que será linha por linha tão rápido quanto os idiomas com primitivas dedicadas. Em outras palavras, não se iluda com visões de um compilador suficientemente inteligente .
fonte
Não.
A outra questão é que tipos fundamentais tendem a ser usados por operações fundamentais. O compilador precisa saber que
int + int
não será compilado em uma chamada de função, mas em alguma instrução elementar da CPU (ou código de byte equivalente). Nesse ponto, se você tiver oint
objeto como normal, terá que efetivamente desmarcar a coisa de qualquer maneira.Esse tipo de operação também não funciona muito bem com subtipagem. Você não pode enviar para uma instrução de CPU. Você não pode enviar a partir de uma instrução de CPU. Quero dizer, todo o ponto de subtipagem é para que você possa usar um
D
onde você pode aB
. As instruções da CPU não são polimórficas. Para que os primitivos façam isso, é necessário agrupar suas operações com uma lógica de despacho que custa várias vezes a quantidade de operações como uma adição simples (ou qualquer outra coisa). O benefício deint
fazer parte da hierarquia de tipos se torna um pouco discutível quando é selado / final. E isso é ignorar todas as dores de cabeça com lógica de despacho para operadores binários ...Basicamente, os tipos primitivos precisariam ter muitas regras especiais sobre como o compilador lida com eles e o que o usuário pode fazer com seus tipos de qualquer maneira ; portanto, muitas vezes é mais simples tratá-los como completamente distintos.
fonte
int + int
pode ser um operador regular no nível do idioma que chama uma instrução intrínseca que é garantida para compilar (ou se comportar como) a adição de número inteiro de CPU nativa op. O benefício deint
herdarobject
não é apenas a possibilidade de herdar outro tipoint
, mas também a possibilidade de umint
comportamento comoobject
sem boxe. Considere os genéricos de C #: você pode ter covariância e contravariância, mas elas são aplicáveis apenas aos tipos de classe - os tipos de estrutura são excluídos automaticamente, porque só podem se tornarobject
através de boxe (implícito, gerado pelo compilador).Existem muito poucos casos em que você precisa que “tipos fundamentais” sejam objetos completos (aqui, um objeto são dados que contêm um ponteiro para um mecanismo de despacho ou são marcados com um tipo que pode ser usado por um mecanismo de despacho):
Você deseja que tipos definidos pelo usuário possam herdar de tipos fundamentais. Isso geralmente não é desejado, pois introduz dores de cabeça relacionadas a desempenho e segurança. É um problema de desempenho porque a compilação não pode assumir que um
int
terá um tamanho fixo específico ou que nenhum método foi substituído e é um problema de segurança porque a semântica deint
s pode ser subvertida (considere um número inteiro igual a qualquer número ou que muda seu valor em vez de ser imutável).Seus tipos primitivos têm supertipos e você deseja ter variáveis com o tipo de um supertipo de um tipo primitivo. Por exemplo, suponha que
int
s sejaHashable
e você deseja declarar uma função que aceita umHashable
parâmetro que pode receber objetos regulares, mas tambémint
s.Isso pode ser “resolvido” tornando esses tipos ilegais: livre-se das subtipagens e decida que as interfaces não são tipos, mas restrições de tipo. Obviamente, isso reduz a expressividade do seu sistema de tipos, e esse tipo de sistema não seria mais chamado de orientado a objetos. Veja Haskell para uma linguagem que usa essa estratégia. C ++ está no meio do caminho, porque tipos primitivos não têm supertipos.
A alternativa é boxe total ou parcial de tipos fundamentais. O tipo de boxe não precisa ser visível pelo usuário. Essencialmente, você define um tipo de caixa interno para cada tipo fundamental e conversões implícitas entre o tipo de caixa e o tipo fundamental. Isso pode ficar estranho se os tipos de caixa tiverem semânticas diferentes. Java apresenta dois problemas: os tipos em caixa têm um conceito de identidade, enquanto os primitivos têm apenas um conceito de equivalência de valor, e os tipos em caixa são anuláveis, enquanto os primitivos são sempre válidos. Esses problemas são completamente evitáveis ao não oferecer um conceito de identidade para tipos de valor, sobrecarregar o operador e não tornar todos os objetos anuláveis por padrão.
Você não possui digitação estática. Uma variável pode conter qualquer valor, incluindo tipos ou objetos primitivos. Portanto, todos os tipos primitivos precisam estar sempre em caixas para garantir uma digitação forte.
Os idiomas que possuem digitação estática fazem bem em usar tipos primitivos sempre que possível e só retornam aos tipos em caixas como último recurso. Embora muitos programas não sejam tremendamente sensíveis ao desempenho, há casos em que o tamanho e a composição dos tipos primitivos são extremamente relevantes: Pense em um processamento de números em larga escala em que você precisa encaixar bilhões de pontos de dados na memória. Mudando de
double
parafloat
pode ser uma estratégia viável de otimização de espaço em C, mas não terá quase nenhum efeito se todos os tipos numéricos estiverem sempre em caixa (e, portanto, desperdiçar pelo menos metade da memória para um ponteiro de mecanismo de despacho). Quando tipos primitivos in a box são usados localmente, é bastante simples removê-lo através do uso de intrínsecas ao compilador, mas seria míope apostar o desempenho geral do seu idioma em um "compilador suficientemente avançado".fonte
int
dificilmente é imutável em todas as línguas.int
valor é imutável, mas umaint
variável não.get rid of subtyping and decide that interfaces aren't types but type constraints.... such a type system wouldn't be called object-oriented any longer
mas isso soa como programação baseada em protótipo, que é definitivamente OOP.A maioria das implementações que conheço impõe três restrições a essas classes que permitem que o compilador use com eficiência os tipos primitivos como representação subjacente na grande maioria das vezes. Essas restrições são:
As situações em que um compilador precisa colocar uma primitiva em um objeto na representação subjacente são relativamente raras, como quando uma
Object
referência está apontando para ele.Isso adiciona um pouco de tratamento especial de caso no compilador, mas não se limita apenas a algum compilador mítico super avançado. Essa otimização está em compiladores de produção reais nos principais idiomas. Scala ainda permite que você defina suas próprias classes de valor.
fonte
No Smalltalk, todos eles (int, float etc.) são objetos de primeira classe. O único caso especial é que SmallIntegers são codificados e tratados de forma diferente pela Máquina Virtual por uma questão de eficiência e, portanto, a classe SmallInteger não admitirá subclasses (o que não é uma limitação prática). Observe que isso não requer nenhuma consideração especial por parte do programador, já que a distinção é limitada a rotinas automáticas como geração de código ou coleta de lixo.
O compilador Smalltalk (código fonte -> bytecodes da VM) e o nativizador da VM (bytecodes -> código da máquina) otimizam o código gerado (JIT) para reduzir a penalidade de operações elementares com esses objetos básicos.
fonte
Eu estava projetando uma linguagem OO e tempo de execução (isso falhou por um conjunto de razões completamente diferente).
Não há nada inerentemente errado em criar coisas como int true classes; na verdade, isso facilita o design do GC, pois agora existem apenas 2 tipos de cabeçalhos de heap (classe e matriz) em vez de 3 (classe, matriz e primitivo) [o fato de podermos mesclar classe e matriz depois que isso não for relevante ]
O caso realmente importante em que os tipos primitivos devem ter métodos final / selados (+ realmente importa, ToString não muito). Isso permite que o compilador resolva estática quase todas as chamadas para as próprias funções e as inline. Na maioria dos casos, isso não importa como comportamento de cópia (optei por disponibilizar a incorporação no nível do idioma [o mesmo fez o .NET]), mas em alguns casos, se os métodos não forem selados, o compilador será forçado a gerar a chamada para a função usada para implementar int + int.
fonte