Diferença entre const e readonly no C#
Os campos const e readonly possuem algumas características parecidas, o objetivo dessa publicação é entender suas diferenças e quando utiliza-las, mostrando um pouco de como elas se comportam.

Em resumo, quando temos certeza que a constante NUNCA vai mudar, podemos usar o tipo const, que por ser executada em tempo de compilação, se torna um pouco mais rápida quando comparado com readonly que é executada em tempo de execução, por outro lado se esse valor pode mudar um dia, use readonly por ser mais flexível e permitir o uso de qualquer tipo.
Mas na prática, o que muda? Qual a diferença em ser resolvido em tempo de compilação ou tempo de execução?
Usei o ILSpy e o Sharplab para analisar o código gerado pelo compilador (MSIL) e descrever a diferença entre os dois cenários.

No exemplo Console.WriteLine(20), o comportamento foi o mesmo que Console.WriteLine(MinhaConstante), isso porque a constante foi resolvida em tempo de compilação, logo podemos dizer que não existe diferença entre Console.WriteLine(20) e Console.WriteLine(MinhaConstante), o código MSIL gerado é exatamente o mesmo para ambos os cenários.
Console.WhiteLine(20):
IL_001b: ldc.i4.s 20
IL_001d: call void [System.Console]System.Console::WriteLine(int32)
Console.WriteLine(MinhaConstante):
IL_000a: ldc.i4.s 10
IL_000c: call void [System.Console]System.Console::WriteLine(int32)
Por outro lado, quando olhamos para o Console.WriteLine(MeuReadOnly) podemos confirmar que ele não tem o seu resultado no MSIL, pois logo na inicialização da classe, o valor da variável readonly é enviado para a stack e recuperado apenas durante a execução do método, ou seja, em tempo de execução.
Console.WriteLine(MeuReadOnly):
IL_002c: ldarg.0
IL_002d: ldfld int32 C::MeuReadOnly
IL_0032: call void [System.Console]System.Console::WriteLine(int32)
Uma nota interessante a respeito do compilador (roslyn) percebida durante essa análise, é que quando estamos fazendo uma operação com valores resolvidos em tempo de compilação (constantes ou valores explícitos), é enviado para a memória stack o valor do resultado, considerando que durante a compilação ele já tem os valores para realizar a operação, isso se torna possível, ou seja, 10 + 10 é resolvido em tempo de compilação e enviado o valor 20 para a stack.
Console.WriteLine(MinhaConstante + MinhaConstante):
IL_0047: ldc.i4.s 20
IL_0049: call void [System.Console]System.Console::WriteLine(int32)
Avaliando os casos onde a soma é 10 + valor resolvido em tempo de execução, o valor da constante é enviado para a stack (10) e também é enviado a referência para o valor resolvido em tempo de execução, e nesses casos a operação é realizada em tempo de execução pois só temos o valor 10 até esse momento, o segundo valor precisa ser consultado em memória.
Console.WriteLine(MinhaConstante + MeuReadOnly):
IL_004e: ldc.i4.s 10
IL_0050: ldarg.0
IL_0051: ldfld int32 C::MeuReadOnly
IL_0056: add
IL_0057: call void [System.Console]System.Console::WriteLine(int32)
Lembrando que os testes foram realizados usando o roslyn na sua última versão (versão do dia 05/08/2021).
Referencias: