Por que o novo tipo de Tupla em .Net 4.0 é um tipo de referência (classe) e não um tipo de valor (estrutura)

88

Alguém sabe a resposta e / ou tem alguma opinião a respeito?

Como as tuplas normalmente não seriam muito grandes, eu presumiria que faria mais sentido usar structs do que classes para elas. O que diz você?

Bent Rasmussen
fonte
1
Para qualquer um que tropeçar aqui depois de 2016. No c # 7 e mais recentes, os literais Tupla são da família de tipos ValueTuple<...>. Consulte a referência em tipos de tupla C #
Tamir Daniely

Respostas:

93

A Microsoft fez com que todos os tipos de tupla referissem tipos no interesse da simplicidade.

Pessoalmente, acho que isso foi um erro. Tuplas com mais de 4 campos são muito incomuns e devem ser substituídas por uma alternativa mais tipificada de qualquer maneira (como um tipo de registro em F #), portanto, apenas pequenas tuplas são de interesse prático. Meus próprios benchmarks mostraram que tuplas não encaixotadas de até 512 bytes ainda podem ser mais rápidas do que tuplas encaixotadas.

Embora a eficiência da memória seja uma preocupação, acredito que o problema dominante é a sobrecarga do coletor de lixo .NET. A alocação e a coleta são muito caras no .NET porque seu coletor de lixo não foi altamente otimizado (por exemplo, em comparação com o JVM). Além disso, o .NET GC (estação de trabalho) padrão ainda não foi paralelizado. Conseqüentemente, programas paralelos que usam tuplas são interrompidos enquanto todos os núcleos disputam o coletor de lixo compartilhado, destruindo a escalabilidade. Essa não é apenas a preocupação dominante, mas a AFAIK foi completamente negligenciada pela Microsoft quando examinou o problema.

Outra preocupação é o envio virtual. Os tipos de referência oferecem suporte a subtipos e, portanto, seus membros são normalmente chamados por meio de envio virtual. Em contraste, os tipos de valor não podem oferecer suporte a subtipos, portanto, a chamada de membro é totalmente inequívoca e pode sempre ser executada como uma chamada de função direta. O envio virtual é extremamente caro no hardware moderno porque a CPU não pode prever onde o contador do programa irá parar. A JVM faz um grande esforço para otimizar o envio virtual, mas o .NET não. No entanto, o .NET oferece uma fuga do envio virtual na forma de tipos de valor. Portanto, representar tuplas como tipos de valor poderia, novamente, ter melhorado dramaticamente o desempenho aqui. Por exemplo, ligandoGetHashCode em uma tupla de 2, um milhão de vezes leva 0,17s, mas chamá-la em uma estrutura equivalente leva apenas 0,008s, ou seja, o tipo de valor é 20 × mais rápido que o tipo de referência.

Uma situação real em que esses problemas de desempenho com tuplas comumente surgem é no uso de tuplas como chaves em dicionários. Na verdade, tropecei neste tópico seguindo um link da questão Stack Overflow F # executa meu algoritmo mais lento do que Python! onde o programa F # do autor acabou sendo mais lento do que seu Python precisamente porque ele estava usando tuplas encaixotadas. Desembalar manualmente usando um structtipo escrito à mão torna seu programa F # várias vezes mais rápido e mais rápido do que Python. Esses problemas nunca teriam surgido se as tuplas fossem representadas por tipos de valor e não por tipos de referência para começar ...

JD
fonte
2
@Bent: Sim, é exatamente o que eu faço quando encontro tuplas em um caminho ativo no F #. Seria bom se eles tivessem fornecido tuplas encaixotadas e não encaixotadas no .NET Framework ...
JD
18
Em relação ao envio virtual, acho que a sua culpa está equivocada: os Tuple<_,...,_>tipos poderiam ter sido lacrados, caso em que nenhum envio virtual seria necessário, apesar de serem tipos de referência. Estou mais curioso para saber por que eles não são selados do que por que são tipos de referência.
kvb
2
Do meu teste, para o cenário em que uma tupla seria gerada em uma função e retornada para outra função e nunca mais usada novamente, as estruturas de campo exposto parecem oferecer um desempenho superior para itens de dados de qualquer tamanho que não sejam tão grandes a ponto de explodir a pilha. Classes imutáveis ​​só são melhores se as referências forem repassadas o suficiente para justificar seu custo de construção (quanto maior o item de dados, menos precisam ser repassadas para que a compensação as favoreça). Visto que uma tupla deve representar simplesmente um grupo de variáveis ​​agrupadas, uma estrutura pareceria ideal.
supercat de
2
"tuplas unboxed de até 512 bytes ainda poderiam ser mais rápidas do que boxed" - que cenário é esse? Você pode conseguir alocar uma estrutura de 512B mais rápido do que uma instância de classe contendo 512B de dados, mas transmiti-la seria mais de 100 vezes mais lento (presumindo-se x86). Há algo que estou esquecendo?
Groo
45

A razão é mais provável porque apenas as tuplas menores fariam sentido como tipos de valor, uma vez que teriam uma pequena área de cobertura de memória. As tuplas maiores (ou seja, aquelas com mais propriedades) sofreriam de fato no desempenho, pois seriam maiores que 16 bytes.

Em vez de algumas tuplas serem tipos de valor e outras, tipos de referência e forçar os desenvolvedores a saber quais são quais, imagino que o pessoal da Microsoft pensasse que torná-los todos os tipos de referência era mais simples.

Ah, suspeitas confirmadas! Consulte Construindo Tupla :

A primeira grande decisão foi tratar as tuplas como um tipo de referência ou de valor. Como eles são imutáveis ​​sempre que você quiser alterar os valores de uma tupla, será necessário criar uma nova. Se eles forem tipos de referência, isso significa que pode haver muito lixo gerado se você estiver alterando elementos em uma tupla em um loop fechado. As tuplas F # eram tipos de referência, mas havia um sentimento da equipe de que eles poderiam perceber uma melhoria de desempenho se duas, e talvez três, tuplas de elemento fossem tipos de valor. Algumas equipes que criaram tuplas internas usaram valor em vez de tipos de referência, porque seus cenários eram muito sensíveis à criação de muitos objetos gerenciados. Eles descobriram que usar um tipo de valor lhes proporcionou um melhor desempenho. Em nosso primeiro rascunho da especificação da tupla, mantivemos as tuplas de dois, três e quatro elementos como tipos de valor, com o restante sendo tipos de referência. No entanto, durante uma reunião de design que incluiu representantes de outras línguas, foi decidido que esse design "dividido" seria confuso, devido à semântica ligeiramente diferente entre os dois tipos. A consistência no comportamento e no design foi considerada de maior prioridade do que aumentos de desempenho em potencial. Com base nessa entrada, alteramos o design para que todas as tuplas sejam tipos de referência, embora tenhamos pedido à equipe F # para fazer alguma investigação de desempenho para ver se experimentou um aumento de velocidade ao usar um tipo de valor para alguns tamanhos de tuplas. Teve uma boa maneira de testar isso, já que seu compilador, escrito em F #, foi um bom exemplo de um grande programa que usava tuplas em uma variedade de cenários. No final, a equipe do F # descobriu que não obteve uma melhoria de desempenho quando algumas tuplas eram tipos de valor em vez de tipos de referência. Isso nos fez sentir melhor sobre nossa decisão de usar tipos de referência para tupla.

Andrew Hare
fonte
3
Grande discussão aqui: blogs.msdn.com/bclteam/archive/2009/07/07/…
Keith Adler,
Ahh, entendi. Ainda estou um pouco confuso que os tipos de valor não significam nada na prática aqui: P
Bent Rasmussen
Acabei de ler o comentário sobre nenhuma interface genérica e, ao examinar o código anterior, foi exatamente outra coisa que me impressionou. É realmente pouco inspirador como os tipos de Tupla são mesquinhos. Mas, acho que você sempre pode fazer o seu próprio ... Não há suporte sintático em C # de qualquer maneira. Ainda assim, pelo menos ... Ainda assim, o uso de genéricos e as restrições que ele tem ainda parecem limitados em .Net. Há um potencial substancial para bibliotecas muito abstratas muito genéricas, mas os genéricos provavelmente precisam de coisas extras como tipos de retorno covariante.
Bent Rasmussen,
7
Seu limite de "16 bytes" é falso. Quando testei isso no .NET 4, descobri que o GC é tão lento que tuplas unboxed de até 512 bytes ainda podem ser mais rápidas. Eu também questionaria os resultados de benchmark da Microsoft. Aposto que eles ignoraram o paralelismo (o compilador F # não é paralelo) e é aí que evitar GC realmente compensa, porque a estação de trabalho GC do .NET também não é paralela.
JD de
Por curiosidade, eu me pergunto se a equipe do compilador testou a ideia de fazer tuplas serem estruturas EXPOSED-FIELD ? Se alguém tem uma instância de um tipo com várias características e precisa de uma instância que é idêntica, exceto por uma característica que é diferente, uma estrutura de campo exposto pode realizar isso muito mais rápido do que qualquer outro tipo, e a vantagem só cresce à medida que as estruturas obtêm Maior.
supercat
7

Se os tipos .NET System.Tuple <...> fossem definidos como estruturas, eles não seriam escalonáveis. Por exemplo, uma tupla ternária de inteiros longos atualmente é dimensionada da seguinte maneira:

type Tuple3 = System.Tuple<int64, int64, int64>
type Tuple33 = System.Tuple<Tuple3, Tuple3, Tuple3>
sizeof<Tuple3> // Gets 4
sizeof<Tuple33> // Gets 4

Se a tupla ternária fosse definida como uma estrutura, o resultado seria o seguinte (com base em um exemplo de teste que implementei):

sizeof<Tuple3> // Would get 32
sizeof<Tuple33> // Would get 104

Como as tuplas têm suporte de sintaxe embutido no F # e são usadas com extrema freqüência nesta linguagem, as tuplas "struct" colocariam os programadores em F # em risco de escrever programas ineficientes sem nem mesmo estarem cientes disso. Isso aconteceria tão facilmente:

let t3 = 1L, 2L, 3L
let t33 = t3, t3, t3

Na minha opinião, tuplas "struct" causariam uma alta probabilidade de criar ineficiências significativas na programação diária. Por outro lado, as tuplas de "classe" existentes atualmente também causam certas ineficiências, conforme mencionado por @Jon. No entanto, acho que o produto da "probabilidade de ocorrência" vezes "dano potencial" seria muito maior com structs do que atualmente com classes. Portanto, a implementação atual é o mal menor.

Idealmente, haveria tuplas de "classe" e tuplas de "estrutura", ambas com suporte sintático em F #!

Editar (07-10-2017)

As tuplas estruturais agora são totalmente suportadas da seguinte maneira:

Marc Sigrist
fonte
2
Se evitarmos a cópia desnecessária, uma estrutura de campo exposto de qualquer tamanho será mais eficiente do que uma classe imutável do mesmo tamanho, a menos que cada instância seja copiada vezes o suficiente para que o custo de tal cópia supere o custo de criação de um objeto heap (o o número de equilíbrio de cópias varia com o tamanho do objeto). Essa cópia pode ser inevitável se alguém quiser uma estrutura que finge ser imutável, mas estruturas que são projetadas para aparecer como coleções de variáveis ​​(que é o que estrutura são ) podem ser usadas com eficiência, mesmo quando são enormes.
supercat
2
Pode ser que o F # não combine bem com a ideia de passar structs refou não goste do fato de que as chamadas "structs imutáveis" não combinem, especialmente quando encaixadas. É uma pena. .Net nunca implementou o conceito de passagem de parâmetros por um executável const ref, já que em muitos casos essa semântica é o que realmente é necessário.
supercat
1
A propósito, considero o custo amortizado de GC como parte do custo de alocação de objetos; se um L0 GC for necessário após cada megabyte de alocações, então o custo de alocar 64 bytes é cerca de 1 / 16.000 do custo de um L0 GC, mais uma fração do custo de qualquer L1 ou L2 GC que se torne necessário como um conseqüência disso.
supercat
4
“Acho que o produto da probabilidade de ocorrência x dano potencial seria muito maior com as estruturas do que atualmente com as classes”. FWIW, muito raramente vi tuplas de tuplas em estado selvagem e as considero uma falha de design, mas muitas vezes vejo as pessoas lutando com um desempenho terrível ao usar tuplas (ref) como chaves em um Dictionary, por exemplo, aqui: stackoverflow.com/questions/5850243 /…
JD
3
@Jon Faz dois anos que escrevi esta resposta e agora concordo com você que seria preferível se pelo menos 2 e 3 tuplas fossem structs. Uma sugestão de voz do usuário do idioma F # foi feita a esse respeito. A questão tem certa urgência, pois tem havido um crescimento maciço de aplicativos em big data, finanças quantitativas e jogos nos últimos anos.
Marc Sigrist,
4

Para 2 tuplas, você ainda pode usar o KeyValuePair <TKey, TValue> de versões anteriores do Common Type System. É um tipo de valor.

Um pequeno esclarecimento para o artigo de Matt Ellis seria que a diferença na semântica de uso entre os tipos de referência e de valor é apenas "leve" quando a imutabilidade está em vigor (o que, é claro, seria o caso aqui). No entanto, acho que teria sido melhor no projeto BCL não introduzir a confusão de ter Tuple passando para um tipo de referência em algum limiar.

Glenn Slayden
fonte
Se um valor for usado uma vez depois de ser retornado, uma estrutura de campo exposto de qualquer tamanho superará qualquer outro tipo, desde que não seja tão monstruosamente grande a ponto de explodir a pilha. O custo de construção de um objeto de classe só será recuperado se a referência acabar sendo compartilhada várias vezes. Há momentos em que é útil para um tipo heterogêneo de tamanho fixo de propósito geral ser uma classe, mas há outros momentos em que uma estrutura seria melhor - mesmo para coisas "grandes".
supercat
Obrigado por adicionar esta regra prática útil. Espero, entretanto, que você não tenha entendido mal minha posição: sou um viciado em valores. ( stackoverflow.com/a/14277068 não deve deixar dúvidas).
Glenn Slayden
Os tipos de valor são um dos grandes recursos do .net, mas infelizmente a pessoa que escreveu o msdn dox não reconheceu que há vários casos de uso separados para eles e que diferentes casos de uso devem ter diretrizes diferentes. O estilo de struct msdn recomenda só deve ser usado com structs que representem um valor homogêneo, mas se for necessário representar alguns valores independentes presos com fita adesiva, não se deve usar esse estilo de struct - deve-se usar uma struct com campos públicos expostos.
supercat de
0

Não sei mas se você já usou F # Tuplas fazem parte da linguagem. Se eu fizesse um .dll e retornasse um tipo de Tuplas, seria bom ter um tipo para colocá-lo. Suspeito agora que o F # faz parte da linguagem (.Net 4) algumas modificações no CLR foram feitas para acomodar algumas estruturas comuns em F #

De http://en.wikibooks.org/wiki/F_Sharp_Programming/Tuples_and_Records

let scalarMultiply (s : float) (a, b, c) = (a * s, b * s, c * s);;

val scalarMultiply : float -> float * float * float -> float * float * float

scalarMultiply 5.0 (6.0, 10.0, 20.0);;
val it : float * float * float = (30.0, 50.0, 100.0)
Ciborgue biônico
fonte