Translate

quarta-feira, 21 de agosto de 2013

Como multiplicar sem perder precisão

Conforme o último artigo, na plataforma Intel ocorre problema de precisão para número ponto flutuante quando se tenta multiplicar com o operador padrão "*".

Este problema é bastante severo quando o sistema trata de transações comerciais ou financeiras, porque são dois tipos de transações que não podem ter erro de precisão, senão os números não batem na contabilidade e em casos mais graves, quando se trata de sistemas de recargas, isso é mais crítico ainda porque é a população humana quem sofre o ônus da falha. Claro, existem "n" impactos.

Para contornar o problema em qualquer linguagem, basta usar a seguinte lógica:

      double tempValue = value * 100;
      tempValue = Math.floor(tempValue + 0.5);

Pronto!

Não, vamos fazer uma classe com as funções e testes dos erros e da solução:

Classe FloatToInteger

O nome da classe não tem nada a ver com os tipos numéricos do Java, é apenas uma forma de dizer que esta classe trata de transformar número flutuantes em números inteiros.

Dentro desta classe existem as seguintes funções auxiliares:

  • doubleToLong - está função converte double para long;
  • doubleToInt - está função converte double para int;
  • floatToInt - está função converte float para int;
  • floatToShort - está função converte float para short.

Para todas as funções, os parâmetros são value e scale:

  • value - é o valor flutuante;
  • scale - é o número de casas decimais e value para a função saber como transformar o flutuante em inteiro.

Código

public class FloatToInteger {

public static long doubleToLong(double value, int scale) {  

double tempValue = value * Math.pow(10, scale);
tempValue = Math.floor(tempValue + 0.5);
return (long) tempValue;

}

public static int doubleToInt(double value, int scale) {  

double tempValue = value * Math.pow(10, scale);
tempValue = Math.floor(tempValue + 0.5);
return (int) tempValue;

}

public static int floatToInt(float value, int scale) {  

double tempValue = value * Math.pow(10, scale);
tempValue = Math.floor(tempValue + 0.5);
return (int) tempValue;

}

public static short floatToShort(float value, int scale) {  

double tempValue = value * Math.pow(10, scale);
tempValue = Math.floor(tempValue + 0.5);
return (short) tempValue;

}

public static void main(String[] args) {

troubleSimulation();
solutionSimulation();

}

public static void troubleSimulation() { 

int erros = 0;

/***********************
* Provocar até 5 erros para amostragem do problema
* ao usar a múltiplicação comum
***********************/
double decimalValue = 0.01d;
long singleNumber = 0L;

System.out.println("1) AMOSTRAGEM DO PROBLEMA - Com o problema");

while (true) {

if (decimalValue >= 100.0d) {
break;
}

decimalValue += 0.10d;

singleNumber = (long) (decimalValue * 100); 

if (singleNumber != doubleToLong(decimalValue, 2)) {
if (erros <= 5) {
System.err.println(String.format("  ERROR: Original: %.20f, After: %d", decimalValue, singleNumber));
}
erros++;
}

}

System.out.println(String.format("População: %.0f, Total de erros: %d\n", 100 / 0.10, erros));

}

public static void solutionSimulation() { 

int erros = 0;

/***********************
* Provocar até 5 erros para amostragem do problema
* ao usar a múltiplicação comum
***********************/
double decimalValue = 0.01d;
long singleNumber = 0L;

System.out.println("2) AMOSTRAGEM DE PROBLEMAS - Após com a solução");

while (true) {

if (decimalValue >= 100.0d) {
break;
}

decimalValue += 0.10d;

singleNumber = doubleToLong(decimalValue, 2); 

if (singleNumber != doubleToLong(decimalValue, 2)) {
if (erros <= 100) {
System.err.println(String.format("  ERROR: Original: %.20f, After: %d", decimalValue, singleNumber));
}
erros++;
}

}

System.out.println(String.format("População: %.0f, Total de erros: %d\n", 100 / 0.10, erros));

}

}

Resultado dos testes

Observe o método main, ele invoca duas funções, uma para simular o problema e outro para comprovar a solução.

Resultado

1) AMOSTRAGEM DO PROBLEMA - Com o problema
  ERROR: Original: 0,90999999999999990000, After: 90
  ERROR: Original: 4,81000000000000000000, After: 480
  ERROR: Original: 4,90999999999999900000, After: 490
  ERROR: Original: 5,00999999999999900000, After: 500
  ERROR: Original: 5,10999999999999850000, After: 510
  ERROR: Original: 5,20999999999999800000, After: 520
População: 1000, Total de erros: 385

2) AMOSTRAGEM DE PROBLEMAS - Após com a solução
População: 1000, Total de erros: 0

Boa sorte!

Até mais.

terça-feira, 20 de agosto de 2013

MemCopy - Copiando bytes de forma fácil

Não é muito incomum para o programador Java fazer cópias de trechos de números para um vetor de bytes de tamanhos: 8 bits, 16 bits, 24 bits, 32 bits ou 64 bits para gerar e: guardar o resultado em arquivo binário, enviar mensagem binária para um socket, porta serial ou porta paralela.

Nota: Em C isso é relativamente fácil de fazer por causa de sua vocação original baseada na manipulação de memória, por isso em C tem a função memcpy designada para isso é extremamente versátil.

Normalmente para se fazer isso o programador já converteu números decimais para inteiros: short, int, long ou BigDecimal com uma operação de multiplicação.

Alerta: A multiplicação convencional com operador "*" utilizado para ponto flutuante pode resultar em perda de precisão não só em Java, mas em qualquer linguagem sobre a plataforma Intel por constatação ao deparar com este mesmo problema em C ANSI, C++ e depois em Java sempre na plataforma Intel. Logo mais falarei de como resolver o problema num outro artigo. É importante deixar claro que a divisão com o operador "/" funciona perfeitamente.

A classe a seguir é uma síntese prática do memcpy do C, porém com algumas melhorias porque ela oferece:
  1. tamanhos de cópias pré-estabelecidas conforme as práticas mais comuns no dia-a-dia do programador;
  2. ainda para cada função já possui assinaturas capazes de lidar com os tipos inteiros mais comuns e;
  3. permite a reversão de bytes para casos específicos onde o destino I/O possui um ordem binária reversa a sua.
Vamos para a prática.

A classe MemCopy

import java.math.BigInteger;
import java.util.Arrays;

public class MemCopy {

public static byte copy8(byte[] value) {

byte result = 0;

if (value != null && value.length > 0) {
result = value[0];
}
return result;
}

public static byte copy8(long value) {

return (byte) (value & 0x00000000000000FF);

}

public static byte copy8(int value) {

return (byte) (value & 0x000000FF);

}

public static byte copy8(short value) {

return (byte) (value & 0x00FF);

}

public static byte[] copy16(byte[] value, boolean reverseBytes) {

byte [] result = new byte[2];

if (value == null || value.length < 2) {
return null;
}

if (reverseBytes) {
result[0] = value[0];
result[1] = value[1];
} else {
result[0] = value[1];
result[1] = value[0];
}
return result;
}
public static byte[] copy16(long value, boolean reverseBytes) {
return copy16((short) (value & 0x000000000000FFFFL), reverseBytes);
}

public static byte[] copy16(int value, boolean reverseBytes) {
return copy16((short) (value & 0x0000FFFF), reverseBytes);
}
public static byte[] copy16(short value, boolean reverseBytes) {

byte [] result = new byte[2];
byte [] aux = new byte[2];

aux[0] = (byte) (value & 0x00FF);
aux[1] = (byte) ((value >> 8) & 0x00FF) ;

if (reverseBytes) {
result[1] = aux[0]; 
result[0] = aux[1]; 
} else {
result = aux;
}

return result;

}

public static byte[] copy24(byte[] value, boolean reverseBytes) {

byte [] result = new byte[3];

if (value == null || value.length < 3) {
return null;
}

if (reverseBytes) {
result[0] = value[2];
result[1] = value[1];
result[2] = value[0];
} else {
result[0] = value[0];
result[1] = value[1];
result[2] = value[2];
}
return result;
}
public static byte[] copy24(long value, boolean reverseBytes) {

byte [] result = new byte[3];
byte [] aux = new byte[3];

aux[0] = (byte) (value & 0x000000FF);
aux[1] = (byte) ((value >> 8) & 0x000000FF) ;
aux[2] = (byte) ((value >> 16) & 0x000000FF) ;

if (reverseBytes) {
result[1] = aux[0]; 
result[0] = aux[1]; 
} else {
result = aux;
}

return result;

}

public static byte[] copy32(byte[] value, boolean reverseBytes) {

byte [] result = new byte[4];
if (value == null || value.length < 4) {
return null;
}

if (reverseBytes) {
result[0] = value[3];
result[1] = value[2];
result[2] = value[1];
result[3] = value[0];
} else {
result[0] = value[0];
result[1] = value[1];
result[2] = value[2];
result[3] = value[3];
}
return result;
}
public static byte[] copy32(long value, boolean reverseBytes) {
return copy32((int) (value & 0xFFFFFFFF), reverseBytes);
}

public static byte[] copy32(int value, boolean reverseBytes) {

byte [] result = new byte[4];
byte [] aux = new byte[4];

aux[0] = (byte) (value & 0x000000FF);
aux[1] = (byte) ((value >> 8) & 0x000000FF) ;
aux[2] = (byte) ((value >> 16) & 0x000000FF);
aux[3] = (byte) ((value >> 24) & 0x000000FF);

if (reverseBytes) {
result[3] = aux[0]; 
result[2] = aux[1]; 
result[1] = aux[2]; 
result[0] = aux[3]; 
} else {
result = aux;
}

return result;
}

public static byte[] copy64(byte[] value, boolean reverseBytes) {

byte [] result = new byte[8];

if (value == null || value.length < 8) {
return null;
}

if (reverseBytes) {
result[0] = value[7];
result[1] = value[6];
result[2] = value[5];
result[3] = value[4];
result[4] = value[3];
result[5] = value[2];
result[6] = value[1];
result[7] = value[0];
} else {
result[0] = value[0];
result[1] = value[1];
result[2] = value[2];
result[3] = value[3];
result[4] = value[4];
result[5] = value[5];
result[6] = value[6];
result[7] = value[7];
}
return result;
}
public static byte[] copy64(long value, boolean reverseBytes) {

byte [] result = new byte[8];
byte [] aux = new byte[8];

aux[0] = (byte) (value & 0x00000000000000FFL);
aux[1] = (byte) ((value >> 8) & 0x00000000000000FFL);
aux[2] = (byte) ((value >> 16) & 0x00000000000000FFL);
aux[3] = (byte) ((value >> 24) & 0x00000000000000FFL);
aux[4] = (byte) ((value >> 32) & 0x00000000000000FFL);
aux[5] = (byte) ((value >> 40) & 0x00000000000000FFL);
aux[6] = (byte) ((value >> 48) & 0x00000000000000FFL);
aux[7] = (byte) ((value >> 56) & 0x00000000000000FFL);

if (reverseBytes) {
result[7] = aux[0]; 
result[6] = aux[1]; 
result[5] = aux[2]; 
result[4] = aux[3]; 
result[3] = aux[4]; 
result[2] = aux[5]; 
result[1] = aux[6]; 
result[0] = aux[7]; 
} else {
result = aux;
}

return result;

}

public static byte[] copy64(BigInteger value, boolean reverseBytes) {

if (value == null)
return null;

byte [] aux = value.toByteArray();
if (aux == null || aux.length == 0)
return null;

byte [] result = new byte[8];

Arrays.fill(result, (byte) 0);

if (aux.length < 8) {
System.arraycopy(aux, 0, result, result.length-aux.length, aux.length);
} else {
System.arraycopy(aux, 0, result, 0, result.length);
}

aux = new byte[8];
System.arraycopy(result, 0, aux, 0, aux.length);
if (!reverseBytes) {
result[7] = aux[0]; 
result[6] = aux[1]; 
result[5] = aux[2]; 
result[4] = aux[3]; 
result[3] = aux[4]; 
result[2] = aux[5]; 
result[1] = aux[6]; 
result[0] = aux[7]; 
} else {
result = aux;
}

return result;

}
}

Teste da classe

O teste da classe requer as funções de conversão de array de bytes para BigInteger, porque é o maior tipo entre os testes e isso confere a capacidade de realizar os testes rápidos com todos os tamanhos de cópias, e ainda requer a função para converter array de bytes para hexadecimal para comprovar os testes.

import java.math.BigInteger;

public class TestMemCopy {

public static BigInteger byteArrayToBigInteger(byte[] b, boolean reverseBytes) {

if (b == null || b.length == 0) {
return null;
}

byte aux[] = new byte[b.length];

if (!reverseBytes) {
for (int i = 0; i < b.length; i++) {
aux[aux.length-1-i] = b[i];
}
} else {
System.arraycopy(b, 0, aux, 0, b.length);
}

return new BigInteger(aux);

}

public static String byteArrayToHex(byte[] bytes, boolean useSpace) {

StringBuffer retorno = new StringBuffer();;

if (bytes==null || bytes.length==0) {
return retorno.toString();
}

// Com espaço
if (useSpace) {

for (int i=0; i<bytes.length; i++) {
byte valor = bytes[i];
int d1 = valor & 0xF;
d1 += (d1 < 10) ? 48 : 55;
int d2 = (valor & 0xF0) >> 4;
d2 += (d2 < 10) ? 48 : 55;
retorno.append((char) d2);
retorno.append((char) d1);
retorno.append(" ");
}

// Sem espaço
} else {

for (int i=0; i<bytes.length; i++) {
byte valor = bytes[i];
int d1 = valor & 0xF;
d1 += (d1 < 10) ? 48 : 55;
int d2 = (valor & 0xF0) >> 4;
d2 += (d2 < 10) ? 48 : 55;
retorno.append((char) d2);
retorno.append((char) d1);
}

}

return retorno.toString();

}
public static void main(String[] args) {

/*******************************
 * Copiando 1 byte de short
 ******************************/
{
// Variáveis para realizar os testes
byte encoded[] = {0};
short decoded = 0;

// Teste short
short input = (short) 102;
encoded[0] = MemCopy.copy8(input);
decoded = (short) (encoded[0] & 0xFF);

System.out.println("Copiando 1 byte de short");
System.out.println(String.format("Original (short): %d, Encoding (Hex): %s, Decoding (short): %d\n", input, byteArrayToHex(encoded, true) ,decoded));
}

/*******************************
 * Copiando 2 bytes de short - Sem reversão de bytes
 ******************************/
{
// Variáveis para realizar os testes
byte encoded[] = null;
BigInteger decoded = null;

// Teste short
short input = (short) 32767;
encoded = MemCopy.copy16(input, false);
decoded = byteArrayToBigInteger(encoded, false);

System.out.println("Copiando 2 bytes de short - Sem reversão de bytes");
System.out.println(String.format("Original (short): %d, Encoding (Hex): %s, Decoding (BigInteger): %d\n", input, byteArrayToHex(encoded, true) ,decoded));
}

/*******************************
 * Copiando 2 bytes de short - Com reversão de bytes
 ******************************/
{
// Variáveis para realizar os testes
byte encoded[] = null;
BigInteger decoded = null;

// Teste short
short input = (short) 23761;
encoded = MemCopy.copy16(input, true);
decoded = byteArrayToBigInteger(encoded, true);

System.out.println("Copiando 2 bytes de short - Com reversão de bytes");
System.out.println(String.format("Original (short): %d, Encoding (Hex): %s, Decoding (BigInteger): %d\n", input, byteArrayToHex(encoded, true) ,decoded));
}

/*******************************
 * Copiando 4 bytes de int - Sem reversão de bytes
 ******************************/
{
// Variáveis para realizar os testes
byte encoded[] = {0, 0};
BigInteger decoded = null;

// Teste short
int input = 2147483647;
encoded = MemCopy.copy32(input, false);
decoded = byteArrayToBigInteger(encoded, false);

System.out.println("Copiando 4 bytes de int - Sem reversão de bytes");
System.out.println(String.format("Original (int): %d, Encoding (Hex): %s, Decoding (BigInteger): %d\n", input, byteArrayToHex(encoded, true) ,decoded));
}

/*******************************
 * Copiando 4 bytes de int - Com reversão de bytes
 ******************************/
{
// Variáveis para realizar os testes
byte encoded[] = null;
BigInteger decoded = null;

// Teste short
int input = 1157483800;
encoded = MemCopy.copy32(input, true);
decoded = byteArrayToBigInteger(encoded, true);

System.out.println("Copiando 4 bytes de int - Com reversão de bytes");
System.out.println(String.format("Original (int): %d, Encoding (Hex): %s, Decoding (BigInteger): %d\n", input, byteArrayToHex(encoded, true) ,decoded));

}

/*******************************
 * Copiando 8 bytes de long - Sem reversão de bytes
 ******************************/
{
// Variáveis para realizar os testes
byte encoded[] = {0, 0};
BigInteger decoded = null;

// Teste short
long input =  922337203685478L; 
encoded = MemCopy.copy64(input, false);
decoded = byteArrayToBigInteger(encoded, false);

System.out.println("Copiando 8 bytes de long - Sem reversão de bytes");
System.out.println(String.format("Original (long): %d, Encoding (Hex): %s, Decoding (BigInteger): %d\n", input, byteArrayToHex(encoded, true) ,decoded));
}

/*******************************
 * Copiando 8 bytes de long - Com reversão de bytes
 ******************************/
{
// Variáveis para realizar os testes
byte encoded[] = null;
BigInteger decoded = null;

// Teste short
long input =  722337203685478L; 
encoded = MemCopy.copy64(input, true);
decoded = byteArrayToBigInteger(encoded, true);

System.out.println("Copiando 8 bytes de long - Com reversão de bytes");
System.out.println(String.format("Original (long): %d, Encoding (Hex): %s, Decoding (BigInteger): %d\n", input, byteArrayToHex(encoded, true) ,decoded));

}

/*******************************
 * Copiando 8 bytes de long - Com reversão de bytes
 ******************************/
{
// Variáveis para realizar os testes
byte encoded[] = null;
BigInteger decoded = null;

// Teste short
long input =  722337203685478L; 
encoded = MemCopy.copy64(input, true);
decoded = byteArrayToBigInteger(encoded, true);

System.out.println("Copiando 8 bytes de long - Com reversão de bytes");
System.out.println(String.format("Original (long): %d, Encoding (Hex): %s, Decoding (BigInteger): %d\n", input, byteArrayToHex(encoded, true) ,decoded));

}

}

}

Resultado

Copiando 1 byte de short
Original (short): 102, Encoding (Hex): 66 , Decoding (short): 102

Copiando 2 bytes de short - Sem reversão de bytes
Original (short): 32767, Encoding (Hex): FF 7F , Decoding (BigInteger): 32767

Copiando 2 bytes de short - Com reversão de bytes
Original (short): 23761, Encoding (Hex): 5C D1 , Decoding (BigInteger): 23761

Copiando 4 bytes de int - Sem reversão de bytes
Original (int): 2147483647, Encoding (Hex): FF FF FF 7F , Decoding (BigInteger): 2147483647

Copiando 4 bytes de int - Com reversão de bytes
Original (int): 1157483800, Encoding (Hex): 44 FD CD 18 , Decoding (BigInteger): 1157483800

Copiando 8 bytes de long - Sem reversão de bytes
Original (long): 922337203685478, Encoding (Hex): 66 88 63 5D DC 46 03 00 , Decoding (BigInteger): 922337203685478

Copiando 8 bytes de long - Com reversão de bytes
Original (long): 722337203685478, Encoding (Hex): 00 02 90 F6 3C 6F 08 66 , Decoding (BigInteger): 722337203685478


Conforme descrito no alerta, no próximo artigo vamos discutir a respeito da multiplicação de flutuantes e como evitar de vez a perda de precisão nesta operação.

Até mais.