Original Article: VectorLib
Author: OPTIVEC

VectorLib

VectorLib é a parte de funções vetoriais do OptiVec. Este arquivo descreve os princípios básicos das bibliotecas OptiVec e fornece uma visão geral sobre o VectorLib. A nova interface orientada a objetos, VecObj, é descrita no capitulo 3. MatrixLib e CMATH são descritos separadamente.

Conteúdo

1. Introdução
1.1 Porque Programação Vetorizada Compensa no PC
1.1.1 Estratégias Gerais de Optimização OptiVec
1.1.2 Optimização do Multi-Processador
1.1.3 Aparelho de Suporte CUDA
1.1.4 Escolhendo a Biblioteca OptiVec certa
2. Os Elementos OptiVec de Rotinas
2.1 Sinônimos para Alguns Tipos de Dados
2.2 Números complexos
2.3 Tipos de Dados Vetoriais
2.4 Prefixo de Funções Vetoriais
3. C++ apenas: VecObj, a Interface Objeto-Orientada para VectorLib
4. VectorLib Funções e Rotinas: Uma Visão Curta
4.1 Geração, inicialização e desalocação de vetores
4.2 Manipulações orientadas para o Índice
4.3 Interconversões de Tipo de Dados
4.4 Mais sobre Integer Aritméticos
4.5 Funções básicas de vetores complexos
4.6 Funções matemáticas
4.6.1 Arredondamento
4.6.2 Comparações
4.6.3 Manipulação de bits direta
4.6.4 Aritmética básica, Acumulações
4.6.5 Aritmética de vetor geométrico
4.6.6 Poderes
4.6.7 Exponenciais e funções hiperbólicas
4.6.8 Logaritmos
4.6.9 Funções de trigonometria
4.7 Análise
4.8 Processamento de sinal: transformações de Fourier e tópicos relacionados
4.9 Funções estatísticas e blocos de construção
4.10 Data Fitting
4.11 Entrada e saída
4.12 Gráficos
5. Manipulação de erros
5.1 Observações Gerais
5.2 Erros de Inteiro
5.3 Erros de ponto flutuante
5.3.1 C/C++ especifico
5.3.2 Pascal/Delphi especifico
5.3.3 Tipos de erro (C / C ++ e Pascal/Delphi)
5.4 O tratamento de números denormal
5.5 Manipulação avançada de erros: gravação de mensagens em um arquivo
5.6 Mensagens de erro do OptiVec
6. Solução de problemas
7. Incluir-Arquivos e Unidades do OptiVec


1. Introdução

O OptiVec oferece um poderoso conjunto de rotinas para aplicações numericamente exigentes, disponibilizando a filosofia da programação vetorial para linguagens C/C ++ e Pascal/Delphi. Ele serve para superar as limitações do gerenciamento de loop de compiladores convencionais - o que provou ser um dos maiores obstáculos no caminho do programador para uma codificação eficiente para aplicações científicas e de análise de dados.

Em contraste com pacotes integrados como o MatLab ou outros, a OptiVec tem a vantagem de ser incorporada nas linguagens modernas e versáteis C/C ++ e Pascal/Delphi. Tanto o C ++ quanto o Fortran já oferecem algum tipo de processamento de vetores, em virtude de classes de iterador usando modelos (C ++) e funções de campo (Fortran90). Ambos, no entanto, são basicamente um meio conveniente de deixar o compilador escrever o loop para você e, em seguida, compilá-lo para o código ineficiente usual. O mesmo é verdade para a maioria das implementações das populares bibliotecas BLAS (Basic Linear Algebra Subroutine). Em comparação com essas abordagens, o OptiVec é superior principalmente em relação à velocidade de execução - na média por um fator de 2-3, em alguns casos, até oito. O desempenho não está mais limitado pela qualidade do seu compilador, mas sim pela velocidade do processador!

Existe uma certa sobreposição no intervalo de funções oferecidas pela OptiVec e pela BLAS, LINPACK e outras bibliotecas e coleções de código-fonte. No entanto, o último deve ser compilado e, conseqüentemente, seu desempenho é determinado principalmente pela qualidade do compilador escolhido. Para o nosso melhor conhecimento, a OptiVec foi. em 1996, o primeiro produto no mercado que oferece uma vasta biblioteca de funções vetoriais realizada em uma verdadeira implementação do Assembler.

  • Todos os operadores e funções matemáticas de C/C ++ são implementados em forma vetorizada; Além disso, muitas outras funções matemáticas estão incluídas, o que normalmente deveria ser calculado por combinações mais ou menos complicadas de funções existentes. Não só a velocidade de execução, mas também a precisão dos resultados é muito melhorada.
  • São fornecidos blocos de construção para análise de dados estatísticos.
  • Derivados, integrais, esquemas de interpolação estão incluídos.
  • As técnicas rápidas de transformação de Fourier permitem convoluções eficientes, análises de correlação, filtragem espectral e assim por diante.
  • A representação gráfica de dados oferece uma maneira conveniente de monitorar os resultados de cálculos vetoriais.
  • Uma ampla gama de funções de matriz otimizadas, como matriz aritmética, álgebra, decomposições, montagem de dados, etc., é oferecida pela MatrixLib.
    O TensorLib está planejado como uma extensão futura desses conceitos para arrays multidimensionais gerais.
  • Cada função existe para cada tipo de dados para o qual isso é razoável. O tipo de dados é sinalizado pelo prefixo do nome da função. Não são utilizados nomes implícitos de nomes ou outros recursos específicos de C ++, o que torna o OptiVec utilizável na planície C, bem como em programas C ++. Além disso, os nomes e a sintaxe de quase todas as funções são iguais nas linguagens C/C ++ e Pascal/Delphi.
  • Os vetores/matrizes de entrada e saída das rotinas VectorLib e MatrixLib podem ser de tamanho variável e é possível processar apenas uma parte (por exemplo, os primeiros 100 elementos ou cada 10º elemento) de um vetor, que é outra vantagem importante em relação a outros abordagens, onde apenas matrizes inteiras são processadas.
  • Uma nova interface orientada a objetos para C++,, chamada VecObj, encapsula todas as funções vetoriais, oferecendo ainda mais fácil uso e maior segurança de memória.
  • O uso de rotinas OptiVec ao invés de loops pode tornar seu código-fonte muito mais compacto e muito melhor legível.

A ampla gama de rotinas e funções cobertas pela OptiVec, a alta eficiência numérica e a maior facilidade de programação fazem deste pacote uma poderosa ferramenta de programação para aplicações científicas e de análise de dados, concorrendo com (e muitas vezes batendo) muitos sistemas integrados de alto preço, mas embutidos na sua linguagem de programação favorita.

1.1 Porquê Programação Vetorizada Compensa no PC

Para processar matrizes de dados unidimensionais ou "vetores", um programador normalmente escreveria um loop sobre todos os elementos vetoriais. Da mesma forma, matrizes de duas ou mais dimensões ("matrizes" ou "tensores") geralmente são processadas através de loops aninhados sobre os índices em todas as dimensões. A alternativa a este estilo clássico de programação são as funções vetoriais e matriciais.
As funções do vetor atuam em arrays/vetores inteiros em vez de argumentos escalares únicos. Eles são a forma mais conseqüente de "vetorização", ou seja, organização do código do programa (por compiladores inteligentes ou pelo próprio programador) de forma a otimizar o tratamento vetorial.

A vetorização sempre foi a fórmula mágica para supercomputadores com suas arquiteturas paralelas multiprocessador. Nessas arquiteturas, procura-se espalhar o esforço computacional igualmente em relação aos processadores disponíveis, maximizando assim a velocidade de execução. Os algoritmos de "dividir e conquistar" dividem tarefas numéricas mais complicadas em pequenos loops sobre elementos de matrizes. Os compiladores sofisticados então descobrem a forma mais eficiente de distribuir os elementos da matriz entre os processadores. Muitos compiladores de supercomputadores também vêm com um grande conjunto de funções vetoriais e matrizes pré-definidas para muitas tarefas básicas. Essas funções vetorizadas oferecem a melhor maneira de alcançar o rendimento.

Obviamente, o enorme processamento paralelo de, digamos, um Cray não é possível mesmo em PCs modernas com suas modestas configurações de núcleo de 2 ou 4 processadores, e muito menos no PC clássico de processador único. Consequentemente, à primeira vista, pode parecer difícil aplicar o princípio da programação vetorizada ao PC. Na verdade, no entanto, existem muitas otimizações específicas de vetores, mesmo para computadores com apenas um CPU. A maioria dessas otimizações não está disponível para os compiladores atuais. Em vez disso, é preciso descer ao nível do código da máquina. Otimizado manualmente, as funções vetoriais do Assembler-written superam os loops compilados por um fator de dois a três, em média. Isso significa que a vetorização, corretamente feita, vale mesmo o esforço, também para programas de PC.

1.1.1 Estratégias Gerais de Otimização OptiVec OptiVec

Aqui estão as estratégias de otimização mais importantes, empregadas no OptiVec para aumentar o desempenho em qualquer PC (independentemente do número de núcleos de processador):

Preload de constantes
O ponto flutuante, bem como as constantes inteiras, empregadas na avaliação de funções matemáticas, são carregadas em registros fora do loop real e permanecem enquanto forem necessárias. Isso economiza uma grande quantidade de operações de carregamento/descarga que são necessárias se uma função matemática é chamada para cada elemento de um vetor separadamente.

Uso completo da pilha XMM e FPU
Quando necessário, são utilizados os oito (64 bits: todos os dezesseis) registros XMM e/ou os oito registros de coprocessador.

Prefetch de pedaços de elementos vetoriais
Começando com o processador Pentium III, a Intel apresentou a característica muito útil do prefetch de memória explícita. Com esses comandos, é possível "informar" o processador para obter dados da memória com suficiente antecedência, de modo que não é desperdiçado tempo esperando por eles quando eles realmente são necessários.

Uso de comandos SIMD
Você pode se perguntar por que essa estratégia não está listada primeiro. O SSE ou "Streaming Single-Instruction-Multiple-Data Extensions", introduzido desde os dias do Pentium III e melhorado com cada nova geração de processador, fornece suporte explícito para programação vetorial com dados de ponto flutuante em flutuador/precisão simples ou dupla. A primeira vista, portanto, eles devem revolucionar a programação vetorial. Dada a relação usual entre as velocidades do processador e do bus de dados, no entanto, muitas das operações aritméticas simples são de transferência de dados limitadas e o uso de comandos SIMD não faz a grande diferença (em relação ao código FPU bem escrito) poderia fazer o contrário . Em muitos casos, a vantagem de usar uma instrução SIMD em vez de instruções separadas da FPU derrete até um aumento de velocidade de 20-30% (o que não é tão ruim assim mesmo!). Para operações mais complicadas, por outro lado, os comandos SIMD muitas vezes não podem ser empregados, quer porque os ramos condicionais devem ser tomados para cada elemento vetorial individualmente, ou porque a precisão e o alcance "extra", disponíveis pelos comandos FPU tradicionais (com seus controles internos precisão estendida), permite simplificar algoritmos tanto quanto o código FPU ainda é mais rápido. Como conseqüência, usamos comandos SIMD somente onde um ganho de velocidade real é possível. Por favor, note, no entanto, que as versões de biblioteca que empregam SIMD (P8, P9 etc.) geralmente sacrificam 1-2 dígitos de precisão para obter o ganho de velocidade descrito. Se isso não for aceitável para sua tarefa específica, fique com as bibliotecas P4.

Loop-desenrolando
Onde as instruções do SIMD não podem ser usadas e onde o emparelhamento máximo dos comandos não pode ser alcançado para elementos únicos, os vetores geralmente são processados ​​em pedaços de dois, quatro ou até mais elementos. Isso permite explorar completamente os tubos de execução paralela. Além disso, a quantidade relativa de tempo gasto para o gerenciamento de loop é significativamente reduzida. Em conexão com a pré-busca de dados, descrita acima, a profundidade dos loops desenrolados é geralmente adaptada ao tamanho da linha de cache.

Endereçamento simplificado
O endereçamento de elementos vetoriais ainda é uma importante fonte de ineficiência com os compiladores atuais. Alternando e retrocedendo entre vetores de entrada e saída, é realizada uma grande quantidade de operações de endereçamento redundantes. As definições rigorosas (e fáceis!) De todas as funções OptiVec permitem reduzir essas operações no mínimo.

Substituição de ponto flutuante por comandos integer
Para quaisquer operações com números de ponto flutuante que também podem ser executados usando comandos integer (como copiar, trocar ou comparar valores predefinidos), o método mais rápido é consistentemente empregado.

Controle rigoroso de precisão
Os compiladores C convertem um float em um double Borland Pascal/Delphi até mesmo estendido antes de passar para uma função matemática. Esta abordagem foi útil em momentos em que a memória do disco era um problema muito grande para incluir funções separadas para cada tipo de dados nos arquivos .LIB, mas é simplesmente ineficiente em PCs modernos. Consequentemente, tais conversões implícitas estão presentes nas rotinas OptiVec. Aqui, uma função de um float é calculada para float (ou seja, única) precisão, sem desperdiçar tempo para o cálculo de mais dígitos do que o necessário - o que seria descartado de qualquer maneira. Existe também uma abordagem de força bruta para controle de precisão: você pode chamar V_setFPAccuracy( 1 ); para alternar ativamente a FPU para uma precisão única, se isso for suficiente para um determinado aplicativo. Por isso, a execução pode ser ligeiramente acelerada dos CPUs Pentium. Seja, no entanto, preparado para aceitar uma precisão ainda mais baixa do que a única de seus resultados finais, se você escolher esta opção. Para mais detalhes e precauções, consulte, V_setFPAccuracy.

Codificação completa
Todas as chamadas de função externa são eliminadas dos loops internos do processamento do vetor. Isso economiza o tempo de execução necessário para os pares call / ret e para carregar os parâmetros na pilha.

Correspondência de linhas de cache de variáveis ​​locais
O cache Level-1 de processadores modernos usa linhas de 64 bytes. Muitas funções OptiVec precisam de variáveis ​​locais reais de precisão dobrada ou de precisão estendida na pilha (principalmente para conversões de número inteiro ou de ponto flutuante ou para verificação de intervalo). Os compiladores de 32 bits alinham a pilha em limites de 4 bytes, o que significa que existe uma certa chance de que os 8 bytes de um double ou 10 bytes de um entendido, armazenados na pilha, atravessarão um limite de linha de cache. Isso, por sua vez, levaria a uma penalidade de quebra de linha de cache, deteriorando o desempenho. Conseqüentemente, essas OptiVec funcionam quando isso é um problema, use procedimentos para alinhar suas variáveis ​​locais em 8 bytes (para doubles), 16-byte (para extendidos), ou limites de 64 bytes (para valores XMM e YMM).

Funções desprotegidas e de alcance reduzido
O OptiVec O OptiVec oferece formas alternativas de algumas funções matemáticas, onde você tem a escolha entre a variante totalmente protegida com o tratamento de erros e outra, variante desprotegida sem. No caso das funções de energia inteira, por exemplo, a ausência de verificação de erros permite que as versões desprotegidas sejam vetadas de forma muito mais eficiente. Da mesma forma, as funções seno e coseno podem ser codificadas de forma mais eficiente para argumentos que o usuário pode garantir que se situem na faixa de -2p e +2p. Nestes casos especiais, o tempo de execução pode ser reduzido em até 40%, dependendo do ambiente de hardware. Esta velocidade aumentada sempre deve ser equilibrada em relação ao risco aumentado, no entanto: se algum elemento de entrada fora do intervalo válido for encontrado, as funções de proteção desprotegidas e de alcance reduzido serão bloqueadas sem aviso.

1.1.2 Otimização do Multi-Processador

Suporte de Multithread
Os modernos processadores multi-core permitem que o sistema operacional distribua threads entre os processadores disponíveis, dimensionando o desempenho geral com o número de núcleos de processador disponíveis. Para isso, todas as funções que funcionam em paralelo devem ser impedidas de interferir entre si através de operações de leitura/gravação em variáveis ​​globais. Com poucas exceções (ou seja, as funções de traçado, que precisam usar variáveis ​​globais para armazenar a janela atual e coordenar as configurações do sistema e as funções de ajuste de dados não-lineares), todas as outras funções do OptiVec são reentrantes e podem ser executadas em paralelo.

Ao projetar seu aplicativo multi-thread, você tem duas opções: paralelismo funcional e paralelismo de dados.

Paralelismo Funcional
Se diferentes threads estão realizando tarefas diferentes - são funcionalmente diferentes - fala-se de paralelismo funcional. Como exemplo, considere um segmento que gerencia a entrada/saída do usuário, enquanto outro executa cálculos de fundo. Mesmo em uma CPU de um único núcleo, esse tipo de multi-threading pode oferecer vantagens (por exemplo, a interface do usuário não bloqueia durante cálculos de fundo extensivos, mas ainda possui entrada). Em um computador multi-core, os dois (ou mais) threads podem realmente ser executados simultaneamente nos diferentes núcleos do processador. Em geral, no entanto, o equilíbrio de carga entre os núcleos do processador está longe de ser perfeito: muitas vezes, um processador está funcionando na carga máxima, enquanto outro está sentado ocioso, aguardando a entrada. Ainda assim, multithreading funcional é a melhor opção sempre que suas tarefas numéricas envolvem vetores e matrizes de tamanho pequeno a moderado.

Paralelismo de Dados
Para melhorar o equilíbrio de carga entre os núcleos de processador disponíveis, maximizando o rendimento, é possível empregar processamento paralelo clássico: os dados a serem processados ​​são divididos em vários pedaços, cada segmento obtendo um desses pedaços. Isso é apropriadamente chamado de paralelismo de dados. A utilidade desta abordagem é limitada pelos gastos gerais envolvidos na distribuição de dados e na comunicação thread-to-thread. Além disso, sempre há partes do código que precisam ser processadas sequencialmente e não podem ser paralelizadas. Portanto, o paralelismo de dados vale a pena para vetores e matrizes maiores. Os tamanhos de intervalo típicos variam de cerca de 100 (para o cálculo de funções transcendentais de valores de entrada complexos) para vários 10.000 elementos (como nas funções aritméticas simples). Somente quando seus vetores e matrizes são consideravelmente maiores que esse limite, o desempenho é realmente melhorado em relação a uma abordagem de paralelismo funcional. O impulso rapidamente se aproxima (mas nunca alcança exatamente) do limite teórico de um fator igual ao número de núcleos de processador disponível.

Escolhendo a Biblioteca OptiVec certa
Sempre que você deseja que seu aplicativo seja executado em uma ampla gama de plataformas suportadas, e quando seus vetores e matrizes são apenas de tamanho pequeno a moderado, recomendamos usar Bibliotecas de uso geral, OVVC4.LIB (para MS Visual C ++) , VCF4W.LIB (para Embarcadero / Borland C ++), ou as unidades em OPTIVEC\LIB4 (para Delphi). Essas bibliotecas combinam boa performance com back-compatibilidade com hardware antigo, até 486DX, Pentium, modelos antigos do Athlon. Todos eles são multi-thread seguros e suportam o paralelismo funcional. Se você não precisa de precisão de ponto flutuante completo e da quantidade de compatibilidade com a volta, você pode obter maior desempenho ao mudar para as bibliotecas P6, P7 ou P8 (marcadas pelo número respectivo no nome da biblioteca).

Para vetores/matrizes grandes em máquinas de núcleo simples de Pentium III + on, oferecemos versões que ganham algum desempenho simplesmente ignorando o cache de dados. Estas Bibliotecas de Vetores Grandes são marcadas pela letra "L": OVVC6L.LIB (para MS Visual C ++), VCF6L.LIB (para Borland C ++) ou as unidades em OPTIVEC\LIB6L (para Delphi). Substitua o "6" por "7" para obter as versões Pentium 4+, e assim por diante. Se for mal utilizado para vetores/matrizes menores, as bibliotecas de vetores grandes realizarão significativamente mais lentas do que as bibliotecas de propósito geral!

Finalmente, para grandes vetores/matrizes em máquinas multi-core, nossas novas bibliotecas otimizadas com múltiplos núcleos distribuem ativamente a carga de trabalho sobre os núcleos de processador disponíveis para a execução paralela de dados. Essas bibliotecas são marcadas com a letra "M", como em OVVC7M.LIB (para MS Visual C ++, usando SSE2), VCF4M.LIB (para Borland C ++, precisão FPU completa) ou as unidades em OPTIVEC\LIB8M (para Delphi, usando SSE3). Essas bibliotecas são projetadas para AMD 64 x2, Intel Core2 Duo ou máquinas equipadas com diversos processadores discretos do nível Pentium 4+.
As bibliotecas "M" ainda serão executadas em máquinas de núcleo único, mas - devido à sobrecarga de gerenciamento de thread - um pouco mais lento do que as bibliotecas de uso geral. Embora as bibliotecas "M" sejam projetadas com vetores de média a grande, a penalidade por usá-las com vetores menores é quase insignificante, já que o mecanismo de thread OptiVec executa automaticamente uma função em um único segmento, se o tamanho do vetor for muito pequeno para execução paralela para recuperar o custo envolvido na comunicação thread-to-thread.
Se você usar as bibliotecas "M", seu programa deve chamar V_initMT antes de qualquer uma das funções vetoriais..

1.1.3 Aparelho de Suporte CUDA

As placas gráficas modernas estão equipadas com poderosa capacidade multiprocessador de até vários centos de kernels de processador em paralelo. Nos últimos anos, as interfaces foram desenvolvidas, permitindo explorar esta capacidade de processamento não só para renderização de gráficos, mas também para cálculos gerais. Uma dessas abordagens é o conceito CUDA pela NVIDIA. Praticamente todas as placas gráficas NVIDIA atuais suportam CUDA. Além disso, o NVIDIA oferece hardware dedicado da CUDA com a família de placas "Tesla" e "Fermi". Com as bibliotecas "C" (por exemplo, OVVC8C.LIB), a OptiVec erece uma maneira simples de usar um dispositivo CUDA para cálculos vetoriais / matriciais sem os problemas de programação em CUDA. Há vários pontos a serem considerados:
  • Obviamente, as bibliotecas "C" podem ser usadas apenas com um dispositivo habilitado para CUDA instalado. Isto significa que apenas os produtos NVIDIA são suportados.
  • Fora dos compiladores suportados pelo OptiVec, atualmente, o NVIDIA fornece suporte ao CUDA apenas para o MS Visual C ++. Isso significa que atualmente não há bibliotecas CUDA OptiVec para os compiladores Embarcadero/Borland disponíveis.
  • É necessário ter o driver de exibição mais recente instalado. Mesmo os novos computadores, na maioria das vezes, não possuem os drivers mais recentes. Eles devem ser selecionados e baixados do site da NVIDIA, www.nvidia.com.
  • Já uma placa gráfica sub-100 $ pode aumentar o desempenho de certas funções em um computador com uma CPU de médio alcance por um fator de 10, hardware dedicado por muito mais. No entanto, a combinação de uma CPU de gama alta com uma placa gráfica low-end (como é freqüentemente encontrada em computadores portáteis) só se beneficiará marginalmente das bibliotecas "C".
  • O custo de troca de dados para além da memória da placa principal e da memória gráfica é tão alto que pode ser "devolvido" apenas para vetores e matrizes muito grandes. Por exemplo, para funções matemáticas como o seno ou funções exponenciais, CUDA compensa de 100.000 elementos vetoriais. Para a multiplicação da matriz, o retorno ocorre na região de 200x200 elementos. Todas as funções do OptiVec verificam se o uso do dispositivo CUDA faz sentido e decide, consequentemente, o processamento de fonte para o processador gráfico ou para permanecer na CPU.
  • Usar o CUDA com o OptiVec é tão fácil quanto simplesmente ligar com a biblioteca "C" e com as bibliotecas de importação fornecidas pela NVIDIA. Nenhuma modificação do seu código-fonte é necessária. Por outro lado, eliminando as transferências repetidas de dados para cada função, a programação direta para dispositivos CUDA com o SDK CUDA da nVidia pode levar a um desempenho consideravelmente maior do que o possível com o uso das bibliotecas OptiVec "C".
  • Como o suporte para o ponto flutuante de dupla precisão é mais ou menos restrito às tabelas Tesla e Fermi caras, o OptiVec atualmente usa o dispositivo CUDA apenas para uma precisão única.
  • As bibliotecas OptiVec "C" realmente usam DLLs desenvolvidas pela NVIDIA. Estes devem ser instalados junto com as bibliotecas OptiVec.
  • A NVIDIA pode, em qualquer momento, alterar os termos da licença para suas bibliotecas CUDA, para que possamos, em algum momento, não podermos incluí-las em nossas distribuições e/ou qualquer suporte CUDA.

1.1.4 Escolhendo a Biblioteca OptiVec certa

Sempre que você deseja que seu aplicativo seja executado em uma ampla gama de plataformas suportadas, e quando seus vetores e matrizes são apenas de tamanho pequeno a moderado, recomendamos usar bibliotecas de propósito geral, OVVC4.LIB (para MS Visual C ++) , VCF4W.LIB (Embarcadero / Borland C ++ compilador série), ou as unidades em OPTIVEC \ LIB4 (para Delphi). Essas bibliotecas combinam boa performance com back-compatibilidade com hardware antigo, até mesmo para 486DX, Pentium, Athlon. Todos eles são multi-thread seguros e suportam o paralelismo funcional.Se você não precisa de precisão cheia de ponto flutuante e da quantidade de compatibilidade com a volta, você pode obter maior desempenho ao mudar para as bibliotecas P8 (ou em breve também P9) (marcadas pelo número respectivo no nome da biblioteca).

Para grandes vetores/matrizes em máquinas multi-core, as bibliotecas otimizadas com múltiplos núcleos distribuem ativamente a carga de trabalho sobre os núcleos de processador disponíveis para a execução paralela de dados Essas bibliotecas são marcadas com a letra "M", como em OVVC7M.LIB (para MS Visual C ++, usando SSE2), VCF4M.LIB (para Embarcadero/Borland C ++, precisão FPU completa) ou as unidades em OPTIVEC \ LIB8M (para Delphi, usando SSE3). Essas bibliotecas são projetadas para AMD 64 x2, Intel Core2 Duo ou máquinas equipadas com diversos processadores discretos do nível Pentium 4+. As bibliotecas CUDA são baseadas nas bibliotecas "M" e são marcadas pela letra "C", como, por exemplo, em OVVC8C.LIB.
As bibliotecas "M" e "C" ainda serão executadas em máquinas de um único núcleo, mas - devido à sobrecarga de gerenciamento de thread - um pouco mais lento que as bibliotecas de uso geral. Embora as bibliotecas "M" sejam projetadas com vetores de média a grande, a penalidade por usá-las com vetores menores é quase insignificante, já que o mecanismo de thread OptiVec executa automaticamente uma função em um único segmento, se o tamanho do vetor for muito pequeno para execução paralela para recuperar o custo envolvido na comunicação thread-to-thread.
Se você usar as bibliotecas "M" ou "C", seu programa deve chamar V_initMT(nAvailProcCores) antes de qualquer uma das funções vetoriais.


2. Elementos Rotina de OptiVec

2.1 Sinônimos Para Alguns Tipos de Dados

Para aumentar a versatilidade e a integridade do OptiVec, os tipos de dados adicionais são definidos em < VecLib.h> ou na unidade VecLib:

a) C/C++ apenas:

O tipo de dados ui (abreviação de "índice não assinado") é usado para indexação de vetores e é definido como "int não assinado".

O tipo de dados de número inteiro de 64 bits (__int64 em BC ++ Builder e MS Visual C ++, Int64 em Delphi) é chamado de quad (para "integer quadword") no OptiVec.
Em 32 bits, o tipo quad sempre está assinado. Funções para inteiros de 64 bits não assinados estão disponíveis apenas nas versões de 64 bits do OptiVec.

  • Borland C ++ abaixo do C ++ Builder 2006 apenas para as versões mais antigas do BC, que não suportavam diretamente inteiros de 64 bits, o quad do tipo de dados é implementado como uma estrutura de dois valores de 32 bits. Os números de ponto flutuante (de preferência duplos longos com sua mantissa de 64 bits) devem ser usados ​​como intermediários. As funções de interface necessárias são setquad, quadtod e _quadtold. Alternativamente, as duas metades de 32 bits podem ser explicitamente definidas, como em:
    xq.Hi = 0x00000001UL;
    xq.Lo = 0x2468ABCDUL;

O tipo de dados estendido, que é familiar para programadores Pascal/Delphi, é definido como um sinônimo de "duplo longo" no OptiVec para C / C ++. Como o Visual C ++ não suporta reais de 80 bits, nós definimos extendido como "duplicado" nas versões OptiVec para esse compilador.

b) Delphi apenas:

O tipo de dados Float, que é familiar para os programadores C/C ++, é definido como um sinônimo para Single. Preferimos ter as letras que definem os tipos de dados do número real em proximidade alfabética: "D" para Double, "E" para Extended, e "F" para Float. Conforme mencionado acima, possíveis números reais de 128 bits e 256 bits poderiam encontrar seu lugar nesta série como "G" para Great e "H" para Hyper.

Por motivos históricos (que datam do desenvolvimento do Turbo Pascal), os vários tipos de dados inteiros têm uma nomenclatura bastante confusa no Delphi. Para tornar os prefixos da função derivada compatíveis com as versões C/C ++ do OptiVec, definimos uma série de sinônimos, conforme descrito na tabela a seguir:

tipoDelphi nomesinonimoPrefixo derivado
8 bit signedShortIntByteIntVBI_
8 bit unsignedByteUByteVUB_
16 bit signed SmallIntVSI_
16 bit unsigned WordSmallVUS_
32 bit signed LongIntVLI_
32 bit unsigned ULongVUL_
64 bit signed Int64QuadIntVQI_
64 bit unsigned (x64 version only!)UInt64UQuadVUQ_
16/32 bit signedIntegerVI_
16/32 bit unsignedCardinalUIntVU_

Para ter um tipo de dados booleano disponível que seja do mesmo tamanho que Número inteiro, definimos o tipo IntBool . É equivalente a LongBool em Delphi. Você verá o tipo IntBool como o valor de retorno de muitas funções matemáticas do VectorLib.

2.2 Numeros complexos

Conforme descrito em maior detalhe para CMATH, OptiVec suporta números complexos tanto em formato cartesiano quanto em formato polar.

Se você usar apenas as funções complexas vetorizadas (mas não as funções escalares da CMATH), não é necessário incluir explicitamente a CMATH. Nesse caso, os seguintes tipos de dados complexos são definidos em para C/C ++:
typedef struct { float Re, Im; } fComplex;
typedef struct { double Re, Im; } dComplex;
typedef struct { extended Re, Im; } eComplex;
typedef struct { float Mag, Arg; } fPolar;
typedef struct { double Mag, Arg; } dPolar;
typedef struct { extended Mag, Arg; } ePolar;

As definições correspondentes para Pascal/Delphi estão contidas na unidade VecLib:
type fComplex = record Re, Im: Float; end;
type dComplex = record Re, Im: Double; end;
type eComplex = record Re, Im: Extended; end;
type fPolar = record Mag, Arg: Float; end;
type dPolar = record Mag, Arg: Double; end;
type ePolar = record Mag, Arg: Extended; end;

Se, por exemplo, um número complexo z for declarado como "fComplex z;", as partes reais e imaginárias de z estão disponíveis como z.Re e z.Im, resp. Os números complexos são inicializados, definindo as partes constituintes separadamente para o valor desejado, por exemplo,
z.Re = 3.0; z.Im = 5.7;
p.Mag = 4.0; p.Arg = 0.7;

(é claro, o operador de atribuição é: = em Pascal / Delphi).
Alternativamente, a mesma inicialização pode ser realizada pelas funções fcplx ou fpolr:
C/C++:
z = fcplx( 3.0, 5.7 );
p = fpolr( 4.0, 0.7 );

Pascal/Delphi:
fcplx( z, 3.0, 5.7 );
fpolr( p, 3.0, 5.7 );

Para números complexos de precisão dupla, use dcplx e dpolr, para números complexos de precisão estendida, use ecplx e epolr.
Os ponteiros para arrays ou vetores de números complexos são declarados usando os tipos de dados cfVector, cdVector e ceVector (para complexo cartesiano) e pfVector, pdVector e peVector (para complexo polar) descritos abaixo.

2.3 Tipos de Vetoriais

Definimos, como de costume, um "vetor" como uma matriz unidimensional de dados contendo, pelo menos, um elemento, sendo todos os elementos do mesmo tipo de dados. Usando uma definição mais matemática, um vetor é um tensor de primeira linha. Uma matriz bidimensional (ou seja, um tensor de dois pontos) é denotada como uma "matriz", e as dimensões mais altas são sempre chamadas de "tensores".
Em contraste com outras abordagens, o VectorLib não permite vetores de tamanho zero!

A base de todas as rotinas VectorLib é formada pelos vários tipos de dados vetoriais apresentados abaixo e declarados em < VecLib.h> ou na unidade VecLib. Em contraste com as matrizes estáticas de tamanho fixo, os tipos VectorLib usam alocação de memória dinâmica e permitem tamanhos variados. Devido a esta maior flexibilidade, recomendamos que você use predominantemente o último. Aqui estão eles:

C/C++
typedeffloat *fVector
typedefdouble *dVector
typedefextended *eVector
typedeffComplex *cfVector
typedefdComplex *cdVector
typedefeComplex *ceVector
typedeffPolar *pfVector
typedefdPolar *pdVector
typedefePolar *peVector
typedefint *iVector
typedefbyte *biVector
typedefshort *siVector
typedeflong *liVector
typedefquad *qiVector
typedefunsigned *uVector
typedefunsigned byte *ubVector
typedefunsigned short *usVector
typedefunsigned long *ulVector
typedefuquad *uqVector
typedefui *uiVector
Pascal/Delphi
typefVector= ^Float;
typedVector= ^Double;
typeeVector= ^Extended;
typecfVector= ^fComplex;
typecdVector= ^dComplex;
typeceVector= ^eComplex;
typepfVector= ^fPolar;
typepdVector= ^dPolar;
typepeVector= ^ePolar
typeiVector= ^Integer;
typebiVector= ^ByteInt;
typesiVector= ^SmallInt;
typeliVector= ^LongInt;
typeqiVector= ^QuadInt;
typeuVector= ^UInt;
typeubVector= ^UByte;
typeusVector= ^USmall;
typeulVector= ^ULong;
typeuqVector= ^UQuad;

Internamente, um tipo de dados como fVector significa "ponteiro para flutuar", mas você pode pensar em uma variável declarada como fVector em termos de "vetor de flutuadores".
Nota: em conexão com os programas do Windows, muitas vezes a letra "l" ou "L" é usada para denotar variáveis ​​"long int". Para evitar a confusão, no entanto, o tipo de dados "long int" é sinalizado por "li" ou "LI" e o tipo de dados "não assinado por muito tempo" é sinalizado por "ul" ou "UL". Os conflitos com prefixos para vetores "longos" são evitados, derivando-os do nome de alias "estendido" e usando "e", "ce", "E" e "CE", como descrito acima e a seguir.

C/C++ especifico:
Os elementos vetoriais podem ser acessados ​​com o operador [], como VA[375] = 1.234;
ou pelas funções específicas de tipo VF_element (retorna o valor do elemento vetorial desejado, mas não pode ser usado para substituir o elemento) e VF_Pelement (retorna o ponteiro para um elemento vetorial).
Especialmente para algumas versões anteriores do Borland C (que têm um erro no ponteiro-aritmética), VF_Pelement deve ser usado em vez da sintaxe X+n.
Em seus programas, você pode misturar esses tipos de vetores com as matrizes estáticas do estilo C clássico.
Por exemplo:
float a[100]; /* matriz estática clássica */
fVector b=VF_vector(100); /* VectorLib vector */
VF_equ1( a, 100 ); /* definir os primeiros 100 elementos de a igual a 1,0 */
VF_equC( b, 100, 3.7 ); /* definir os primeiros 100 elementos de b igual a 3.7 */

Pascal / Delphi específico:
Como em C/C ++, você pode misturar esses tipos de vetores com as matrizes estáticas do clássico estilo Pascal. As matrizes estáticas devem ser passadas para as funções do OptiVec com o "endereço" do operador. Aqui, o exemplo acima lê:
a: array[0..99] De único; (* classic static array *)
b: fVector;(* VectorLib vector *)
b := VF_vector(100);
VF_equ1( @a, 100 ); (* Conjunto de 100 elementos de a = 1.0 *)
VF_equC( b, 100, 3.7 ); (* Conjunto de 100 elementos de b = 3.7 *)

O Delphi também oferece matrizes dinamicamente alocadas, que também podem ser usadas como argumentos para as funções do OptiVec. A tabela a seguir compara os vetores baseados em ponteiros do VectorLib com os tipos de matriz de Pascal/Delphi:

OptiVec vectoresPascal/Delphi static/Arrays dinâmicos
alinhamento do primeiro elementono limite de 32 bytes para uma correspondência ideal da linha de cacheLimite de 2 ou 4 bytes (pode causar uma penalidade de quebra de linha para duplo, QuadInt)
alinhamento dos seguintes elementospacked (ou seja, nenhum bytes fofos entre elementos, mesmo para tipos de 8, 10 e 16 bitos arrays devem ser declarados como "packed" para o Delphi 4+ ser compatível com o OptiVec
verificação do intervalo de índicenenhumautomático com informações de tamanho incorporadas
alocação dinâmicafunction VF_vector, VF_vector0Procedimento SetLength (apenas Delphi 4+)
Inicialização com 0Opcional chamando VF_vector0sempre (Delphi 4+ apenas)
de-allocationfunction V_free, V_freeAllProcedimento Finalize (somente Delphi 4+)
lendo elementos únicosfunction VF_element:
a := VF_element(X,5);
Delphi 4+ somente: typecast em array também é possível:
a := fArray(X)[5];
Índice entre parênteses:
a := X[5];
configurando elementos únicosfunction VF_Pelement:
VF_Pelement(X,5)^ := a;
Delphi 4+ somente: typecast em array também é possível:
fArray(X)[5] := a;
Índice entre parênteses:
X[5] := a;
Passando para function OptiVec diretamente:
VF_equ1( X, sz );
endereço do operador:
VF_equ1( @X, sz );
passando o sub-vetor para a function OptiVecfunction VF_Pelement:
VF_equC( VF_Pelement(X,10), sz-10, 3.7);
endereço do operador:
VF_equC( @X[10], sz-10, 3.7 );

Resumindo as propriedades dos vetores OptiVec e das matrizes Pascal/Delphi, estes últimos são um pouco mais convenientes e, devido à verificação do intervalo de índice, mais seguro, enquanto os vetores OptiVec baseados em ponteiros são processados ​​mais rapidamente (devido ao melhor alinhamento e à ausência de rotinas de verificação).

2.4 Prefixos de Funções Vetoriais

Nas versões planície-C, Pascal e Delphi, toda função OptiVec possui um prefixo que denota o tipo de dado no qual atua. (Leia aqui sobre as funções C++ sobrecarregadas do VecObj.)
Prefix Argumentos e Valor de Retorno
VF_fVector e float
VD_dVector e double
VE_eVector e extended (long double)
VCF_cfVector e fComplex
VCD_cdVector e dComplex
VCE_ceVector e eComplex
VPF_pfVector e fPolar
VPD_ pdVector e dPolar
VPE_peVector e ePolar
VI_iVector e int / Integer
VBI_biVector e byte / ByteInt
VSI_siVector e short int / SmallInt
VLI_liVector e long int / LongInt
VQI_qiVector e quad / QuadInt
VU_uVector e unsigned / UInt
VUB_ubVector e unsigned char / UByte
VUS_usVector e unsigned short / USmall
VUL_ulVector e unsigned long / ULong
VUQ_uqVector e uquad / UQuad (for Win64 only!)
VUI_uiVector e ui
V_(Conversões de tipo de dados como V_FtoD, tipo de dados funções independentes como V_initPlot)


3. VecObj, a Interface Objeto-Orientada para VectorLib

VecObj, a interface C++ orientada a objetos para as funções do vetor OptiVec foi escrita por Brian Dale, Case Western Reserve University.
Entre as vantagens que oferece são as seguintes:
  • alocação automática e desalocação da memória
  • Manejo simplificado de vetores
  • risco reduzido de vazamentos de memória
  • maior segurança de acesso à memória
  • Operadores de sobrecarga intuitivos
  • chamadas de função mais simples
Há algumas desvantagens, entre as quais você deve estar ciente:
  • aumento da carga do compilador
  • sobrecarga maior (como para qualquer código C ++ encapsulado!), levando a
  • Tamanho de código aumentado
  • diminuição da eficiência computacional
  • Os vetores podem ser processados ​​apenas como um todo, não em partes
VecObj O VecObj está contido nos arquivos de inclusão < VecObj.h>, < fVecObj.h>, < dVecObj.h>, etc., com um arquivo de inclusão para cada um dos tipos de dados suportados no OptiVec.
Para obter toda a interface (para todos os tipos de dados ao mesmo tempo),
#include <OptiVec.h>.
Para acessar qualquer uma das funções gráficas vetoriais, sempre inclua < OptiVec.h>.

MS Visual C++ e Embarcadero/Borland C ++ Builder (mas não versões anteriores do Borland C ++): os programadores devem colocar a diretiva
"Usando namespace OptiVec;"
quer no corpo de qualquer função que use o VecObj, ou na parte de declaração global do programa. Colocar a diretiva no corpo da função é mais segura, evitando possíveis conflitos de espaço para nome em outras funções.
Os objetos vetoriais são definidos como vetor de classes , encapsulando o endereço do vetor (ponteiro) e o tamanho.
Para facilitar o uso, essas classes obtiveram nomes de alias fVecObj, dVecObj, e assim por diante, com o tipo de dados sinalizado pela primeira ou duas letras do nome da classe, da mesma forma que os tipos de vetores descritos acima.

Todas as funções definidas no VectorLib para um tipo de dado de vetor específico são contidas como funções de membro na respectiva classe tVecObj.
Os construtores estão disponíveis em quatro formas:
vector(); // nenhuma memória alocada, tamanho definido como 0
vector( ui size ); // vetor de elementos de tamanho alocado
vector( ui size, T fill ); // como antes, mas inicializado com o valor "fill"
vector( vector<T> init ); // cria uma cópia do vetor "init"

Para todas as classes de vetores, os operadores aritméticos
+-*/+=-=*=/=
são definidos, com exceção das classes vetoriais complexas polares, onde somente as multiplicações e divisões, mas não são suportadas adições ou subtrações. Esses operadores são os únicos casos em que você pode atribuir diretamente o resultado de um cálculo a um objeto vetorial, como
fVecObj Z = X + Y; or
fVecObj Z = X * 3.5;
Note, no entanto, que as regras de sintaxe de classe C ++ não permitem uma implementação muito eficiente desses operadores. As funções dos membros aritméticos são muito mais rápidas. Se a velocidade for um problema, use
fVecObj Z.addV( X, Y ); or
fVecObj Z.mulC( X, 3.5 );
em vez da sintaxe do operador. O operador * refere-se à multiplicação de elementos, não ao produto escalar de dois vetores.

Todas as outras funções aritméticas e matemáticas só podem ser chamadas como funções de membro do respectivo vetor de saída, como, por exemplo, Y.exp(X). Embora certamente seria mais lógico ter essas funções definidas de tal forma que você pudesse escrever "Y = exp(X)" a sintaxe da função membro foi escolhida para considerações de eficiência: a única maneira de implementar a segunda variante é para armazenar o resultado da função exponencial de X primeiro em um vetor temporário, que é então copiado para Y, aumentando assim consideravelmente as demandas de carga de trabalho e de memória.

Embora a maioria das funções VecObjsejam funções de membros do vetor de saida existe uma série de funções que não possuem um vetor de saída. Nesses casos, as funções são funções de membro de um vetor de entrada.
exemplo: s = X.mean();.

Se você precisar processar um vetor VecObj em uma função "classic" plain-C VectorLib (por exemplo, para processar apenas uma parte dele), você pode usar as funções do membro
getSize() para recuperar seu tamanho,
getVector() para o ponteiro (do tipo de dados tVector, onde "t" representa o prefixo do tipo usual), e
Pelement( n ) para um ponteiro para o elemento n'th.