Estou desenvolvendo algumas simulações de engenharia. Isso envolve a implementação de algumas equações longas, como esta equação para calcular a tensão em um material semelhante a borracha:
T = (
mu * (
pow(l1 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a
* (
pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
- l1 * l2 * l3 * pow(l1 * l2 * l3, -0.4e1 / 0.3e1) / 0.3e1
) * pow(l1 * l2 * l3, 0.1e1 / 0.3e1) / l1
- pow(l2 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l1 / 0.3e1
- pow(l3 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l1 / 0.3e1
) / a
+ K * (l1 * l2 * l3 - 0.1e1) * l2 * l3
) * N1 / l2 / l3
+ (
mu * (
- pow(l1 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l2 / 0.3e1
+ pow(l2 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a
* (
pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
- l1 * l2 * l3 * pow(l1 * l2 * l3, -0.4e1 / 0.3e1) / 0.3e1
) * pow(l1 * l2 * l3, 0.1e1 / 0.3e1) / l2
- pow(l3 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l2 / 0.3e1
) / a
+ K * (l1 * l2 * l3 - 0.1e1) * l1 * l3
) * N2 / l1 / l3
+ (
mu * (
- pow(l1 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l3 / 0.3e1
- pow(l2 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l3 / 0.3e1
+ pow(l3 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a
* (
pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
- l1 * l2 * l3 * pow(l1 * l2 * l3, -0.4e1 / 0.3e1) / 0.3e1
) * pow(l1 * l2 * l3, 0.1e1 / 0.3e1) / l3
) / a
+ K * (l1 * l2 * l3 - 0.1e1) * l1 * l2
) * N3 / l1 / l2;
Eu uso o Maple para gerar o código C ++ para evitar erros (e economizar tempo com álgebra tediosa). Como esse código é executado milhares (senão milhões) de vezes, o desempenho é uma preocupação. Infelizmente, a matemática só simplifica até agora; as longas equações são inevitáveis.
Que abordagem posso adotar para otimizar essa implementação? Estou procurando estratégias de alto nível que devo aplicar ao implementar tais equações, não necessariamente otimizações específicas para o exemplo mostrado acima.
Estou compilando usando g ++ com --enable-optimize=-O3
.
Atualizar:
Eu sei que há muitas expressões repetidas, estou supondo que o compilador iria lidar com elas; meus testes até agora sugerem que sim.
l1, l2, l3, mu, a, K
são todos números reais positivos (não zero).
Tenho substituído l1*l2*l3
com uma variável equivalente: J
. Isso ajudou a melhorar o desempenho.
Substituir pow(x, 0.1e1/0.3e1)
por cbrt(x)
foi uma boa sugestão.
Ele será executado em CPUs. Em um futuro próximo, provavelmente funcionará melhor em GPUs, mas por enquanto essa opção não está disponível.
fonte
pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
por uma variável ... Você precisa avaliar seu código para ter certeza se ele executa rápido ou lento, no entanto.Respostas:
Editar resumo
pow(x, 0.1e1/0.3e1)
é o mesmo quecbrt(x)
.risquei) essas edições e empurrei-as para o final da revisão atual desta resposta. No entanto, não os eliminei. Eu sou humano. É fácil cometermos um erro.l1
,l2
el3
são números reais positivos e sea
é um número real não-zero. (Ainda não ouvimos do OP sobre a natureza específica desses coeficientes. Dada a natureza do problema, essas são suposições razoáveis.)Primeiras coisas primeiro
Maple e Mathematica às vezes perdem o óbvio. Ainda mais importante, os usuários do Maple e do Mathematica às vezes cometem erros. Substituir "frequentemente", ou talvez até "quase sempre", em vez de "às vezes é provavelmente mais perto do que acertar.
Você poderia ter ajudado o Maple a simplificar essa expressão, contando sobre os parâmetros em questão. No exemplo em questão, eu suspeito que
l1
,l2
el3
são números reais positivos e essea
é um número real diferente de zero. Se for esse o caso, diga isso. Esses programas matemáticos simbólicos normalmente assumem que as quantidades disponíveis são complexas. Restringir o domínio permite que o programa faça suposições que não são válidas nos números complexos.Como simplificar essas grandes bagunças de programas de matemática simbólica (esta edição)
Os programas de matemática simbólica normalmente fornecem a capacidade de fornecer informações sobre os vários parâmetros. Use essa habilidade, principalmente se o seu problema envolver divisão ou exponenciação. No exemplo em mãos, você poderia ter ajudado a bordo simplificar essa expressão, dizendo-se que
l1
,l2
el3
são números reais positivos e quea
é um número real não-zero. Se for esse o caso, diga isso. Esses programas matemáticos simbólicos normalmente assumem que as quantidades disponíveis são complexas. Restringir o domínio permite que o programa faça suposições como a x b x = (ab) x . Isso ocorre apenas sea
eb
forem números reais positivos e sex
for real. Não é válido nos números complexos.Em última análise, esses programas matemáticos simbólicos seguem algoritmos. Ajude-o junto. Tente expandir, coletar e simplificar antes de gerar o código. Nesse caso, você poderia ter coletado os termos que envolvem um fator de
mu
e aqueles que envolvem um fator deK
. Reduzir uma expressão à sua "forma mais simples" continua sendo um pouco uma arte.Quando você receber uma bagunça feia de código gerado, não aceite isso como uma verdade que você não deve tocar. Tente simplificar você mesmo. Veja o que o programa matemático simbólico tinha antes de gerar o código. Veja como eu reduzi sua expressão a algo muito mais simples e rápido, e como a resposta de Walter levou a minha vários passos adiante. Não existe receita mágica. Se houvesse uma receita mágica, Maple a teria aplicado e dado a resposta que Walter deu.
Sobre a questão específica
Você está fazendo muitas adições e subtrações nesse cálculo. Você pode entrar em apuros se tiver termos que quase cancelam um ao outro. Você está desperdiçando muita CPU se tiver um termo que domina os outros.
Em seguida, você está desperdiçando muita CPU ao realizar cálculos repetidos. A menos que você tenha ativado
-ffast-math
, o que permite ao compilador quebrar algumas das regras do ponto flutuante IEEE, o compilador não irá (na verdade, não deve) simplificar essa expressão para você. Em vez disso, fará exatamente o que você disse para fazer. No mínimo, você deve calcularl1 * l2 * l3
antes de computar essa bagunça.Por fim, você está fazendo muitas chamadas para
pow
, o que é extremamente lento. Observe que várias dessas chamadas são no formato (l1 * l2 * l3) (1/3) . Muitas dessas chamadas parapow
poderiam ser realizadas com uma única chamada parastd::cbrt
:Com isso,
X * pow(l1 * l2 * l3, 0.1e1 / 0.3e1)
torna-seX * l123_pow_1_3
.X * pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
torna-seX / l123_pow_1_3
.X * pow(l1 * l2 * l3, 0.4e1 / 0.3e1)
torna-seX * l123_pow_4_3
.X * pow(l1 * l2 * l3, -0.4e1 / 0.3e1)
torna-seX / l123_pow_4_3
.Maple não percebeu o óbvio.
Por exemplo, há uma maneira muito mais fácil de escrever
Partindo do princípio de que
l1
,l2
el3
são reais em vez de números complexos, e que a verdadeira raiz cubo (em vez da raiz complexo princípio) estão a ser extraído, o acima reduz aou
Usando em
cbrt_l123
vez del123_pow_1_3
, a expressão desagradável na pergunta se reduz aSempre verifique, mas sempre simplifique também.
Aqui estão alguns dos meus passos para chegar ao acima:
Resposta errada, intencionalmente mantida para humildade
Observe que isso é atingido. Está errado.
AtualizarMaple não percebeu o óbvio. Por exemplo, há uma maneira muito mais fácil de escrever
Partindo do princípio de que
l1
,l2
el3
são reais em vez de números complexos, e que a verdadeira raiz cubo (em vez da raiz complexo princípio) estão a ser extraído, o acima reduz a zero. Este cálculo de zero é repetido várias vezes.Segunda atualização
Se eu fiz as contas certas (não há garantia de que fiz as contas certas), a expressão desagradável na pergunta se reduz a
O acima assume quel1
,l2
el3
são números reais positivos.fonte
-ffast-math
com gcc ou clang), o compilador não pode confiar empow(x,-1.0/3.0)
ser igual ax*pow(x,-4.0/3.0)
. O último pode estar submerso, enquanto o primeiro não. Para ser compatível com o padrão de ponto flutuante, o compilador não deve otimizar esse cálculo para zero.-fno-math-errno
do g ++ para fazerpow
chamadas idênticas do CSE . (A menos que talvez possa provar que o pow não precisará definir errno?)N1
,N2
eN3
são não-negativo, um dos2*N_i-(N_j+N_k)
será negativo, um será positiva, eo outro será em algum lugar no meio. Isso pode facilmente resultar em problemas de cancelamento numérico.A primeira coisa a notar é que
pow
é muito caro, então você deve se livrar disso o máximo possível. Examinando a expressão, vejo muitas repetições depow(l1 * l2 * l3, -0.1e1 / 0.3e1)
epow(l1 * l2 * l3, -0.4e1 / 0.3e1)
. Portanto, eu esperaria um grande ganho da pré-computação desses:onde estou usando a função boost pow .
Além disso, você tem mais alguns
pow
com expoentea
. Sea
for inteiro e conhecido no momento do compilador, você também pode substituí-losboost::math::pow<a>(...)
para obter mais desempenho. Também sugiro substituir termos comoa / l1 / 0.3e1
por,a / (l1 * 0.3e1)
pois a multiplicação é mais rápida do que a divisão.Finalmente, se você usar g ++, poderá usar o
-ffast-math
sinalizador que permite que o otimizador seja mais agressivo na transformação de equações. Leia sobre o que este sinalizador realmente faz , pois tem efeitos colaterais.fonte
-ffast-math
leva o código a ficar instável ou dar respostas erradas. Temos um problema semelhante com os compiladores Intel e precisamos usar a-fp-model precise
opção, caso contrário, o código pode explodir ou fornecer as respostas erradas. Isso-ffast-math
poderia ser mais rápido, mas eu recomendaria prosseguir com muito cuidado com essa opção, além dos efeitos colaterais listados em sua pergunta vinculada.-fno-math-errno
que o g ++ seja capaz de içar chamadas idênticas parapow
fora de um loop. Essa é a parte menos "perigosa" de -ffast-math, para a maioria dos códigos.pow
ser extremamente lento e acabamos usando odlsym
hack mencionado nos comentários para obter aumentos de desempenho consideráveis, quando na verdade podíamos fazer com um pouco menos de precisão.pow
é uma função pura, de acordo com o padrão, porque é para definirerrno
em algumas circunstâncias. Definir sinalizadores como-fno-math-errno
fazer com que ele não seja definidoerrno
(violando o padrão), mas é uma função pura e pode ser otimizado como tal.Uau, que expressão infernal. Criar a expressão com Maple na verdade foi uma escolha abaixo do ideal aqui. O resultado é simplesmente ilegível.
Teoricamente, o compilador deveria ser capaz de fazer tudo isso para você, mas às vezes não pode - por exemplo, quando o aninhamento de loop se espalha por várias funções em unidades de compilação diferentes. De qualquer forma, isso lhe dará um código muito melhor legível, compreensível e sustentável.
fonte
x
e nãoy
são variáveis de uma única letra sem sentido, são palavras inteiras com uma definição precisa e um significado bem e amplamente compreendido.A resposta de David Hammen é boa, mas ainda está longe de ser ideal. Vamos continuar com sua última expressão (no momento em que escrevo isso)
que pode ser otimizado ainda mais. Em particular, podemos evitar a chamada para
cbrt()
e uma das chamadas parapow()
se explorar algumas identidades matemáticas. Vamos fazer isso novamente, passo a passo.Observe que eu também otimizei
2.0*N1
paraN1+N1
etc. Em seguida, podemos fazer com apenas duas chamadas parapow()
.Uma vez que as chamadas para
pow()
são de longe a operação mais cara aqui, vale a pena reduzi-las ao máximo (a próxima operação cara foi a chamada paracbrt()
, que eliminamos).Se por acaso
a
for inteiro, as chamadas parapow
poderiam ser otimizadas para chamadas paracbrt
(mais potências inteiras), ou seathird
for meio-inteiro, podemos usarsqrt
(mais potências inteiras). Além disso, se por acasol1==l2
oul1==l3
oul2==l3
uma ou ambas as chamadas parapow
pode ser eliminado. Portanto, vale a pena considerá-los como casos especiais se tais chances existirem de forma realista.fonte
Já tentei uma simplificação manual dessa fórmula, gostaria de saber se salva alguma coisa?
[ADICIONADO] Trabalhei um pouco mais na fórmula das três últimas linhas e consegui essa beleza:
Deixe-me mostrar meu trabalho, passo a passo:
fonte
std::pow()
, das quais você ainda tem 6, 3 vezes mais do que o necessário. Em outras palavras, seu código é 3 vezes mais lento do que possível.Isso pode ser um pouco conciso, mas eu realmente encontrei uma boa aceleração para polinômios (interpolação de funções de energia) usando Horner Form, que basicamente reescreve
ax^3 + bx^2 + cx + d
comod + x(c + x(b + x(a)))
. Isso evitará muitas chamadas repetidas parapow()
e o impedirá de fazer coisas bobas, como ligar separadamentepow(x,6)
e empow(x,7)
vez de apenas fazerx*pow(x,6)
.Isso não se aplica diretamente ao seu problema atual, mas se você tiver polinômios de alta ordem com potências inteiras, isso pode ajudar. Você deve ter cuidado com a estabilidade numérica e os problemas de estouro, já que a ordem das operações é importante para isso (embora, em geral, eu realmente ache que a Forma de Horner ajuda nisso, já que
x^20
ex
geralmente estão em muitas ordens de magnitude).Também como uma dica prática, se ainda não o fez, tente primeiro simplificar a expressão em maple. Você provavelmente pode fazer com que ele faça a maior parte da eliminação de subexpressão comum para você. Não sei o quanto isso afeta o gerador de código naquele programa em particular, mas sei que no Mathematica fazer um FullSimplify antes de gerar o código pode resultar em uma grande diferença.
fonte
Parece que há muitas operações repetidas em andamento.
Você pode pré-calculá-los para não chamar repetidamente a
pow
função, o que pode ser caro.Você também pode pré-calcular
conforme você usa esse termo repetidamente.
fonte
-ffast-math
esteja habilitado e, conforme observado em um comentário de @ tpg2114, essa otimização pode criar resultados extremamente instáveis.Se você tiver uma placa de vídeo Nvidia CUDA, pode considerar transferir os cálculos para a placa de vídeo - que por si só é mais adequada para cálculos complicados computacionalmente.
https://developer.nvidia.com/how-to-cuda-c-cpp
Caso contrário, você pode querer considerar vários threads para cálculos.
fonte
Por acaso, você poderia fornecer o cálculo simbolicamente. Se houver operações de vetor, você pode realmente querer investigar o uso de blas ou lapack, que em alguns casos pode executar operações em paralelo.
É concebível (correndo o risco de sair do assunto?) Que você possa usar o python com numpy e / ou scipy. Na medida do possível, seus cálculos podem ser mais legíveis.
fonte
Como você perguntou explicitamente sobre otimizações de alto nível, pode valer a pena tentar diferentes compiladores C ++. Hoje em dia, os compiladores são bestas de otimização muito complexas e os fornecedores de CPU podem implementar otimizações muito poderosas e específicas. Mas observe que alguns deles não são gratuitos (mas pode haver um programa acadêmico gratuito).
Já vi trechos de código diferirem na velocidade de execução por um fator de 2, apenas mudando o compilador (com otimizações completas, é claro). Mas esteja ciente de verificar a identidade da saída. A otimização agressiva pode levar a resultados diferentes, algo que você definitivamente deseja evitar.
Boa sorte!
fonte