Pregunta ¿Cómo se convierte una matriz de bytes a una cadena hexadecimal, y viceversa?


¿Cómo se puede convertir una matriz de bytes en una cadena hexadecimal, y viceversa?


1115
2018-03-08 21:56


origen


Respuestas:


Ya sea:

public static string ByteArrayToString(byte[] ba)
{
  StringBuilder hex = new StringBuilder(ba.Length * 2);
  foreach (byte b in ba)
    hex.AppendFormat("{0:x2}", b);
  return hex.ToString();
}

o:

public static string ByteArrayToString(byte[] ba)
{
  return BitConverter.ToString(ba).Replace("-","");
}

Hay incluso más variantes de hacerlo, por ejemplo aquí.

La conversión inversa sería así:

public static byte[] StringToByteArray(String hex)
{
  int NumberChars = hex.Length;
  byte[] bytes = new byte[NumberChars / 2];
  for (int i = 0; i < NumberChars; i += 2)
    bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
  return bytes;
}

Utilizando Substring es la mejor opción en combinación con Convert.ToByte. Ver esta respuesta para más información. Si necesita un mejor rendimiento, debe evitar Convert.ToByte antes de que puedas soltar SubString.


1071



Análisis de rendimiento

Nota: nuevo líder al 2015-08-20.

Ejecuté cada uno de los diversos métodos de conversión a través de Stopwatch pruebas de rendimiento, una ejecución con una oración aleatoria (n = 61, 1000 iteraciones) y una ejecución con un texto de Project Gutenburg (n = 1.238.957, 150 iteraciones). Aquí están los resultados, aproximadamente de más rápido a más lento. Todas las medidas están en tics (10,000 tics = 1 ms) y todas las notas relativas se comparan con la [más lenta] StringBuilder implementación. Para el código usado, vea a continuación o repositorio del marco de prueba donde ahora mantengo el código para ejecutar esto.

Renuncia

ADVERTENCIA: No confíe en estas estadísticas para nada concreto; son simplemente una muestra de corrida de datos de muestra. Si realmente necesita un rendimiento de primera clase, pruebe estos métodos en un entorno representativo de sus necesidades de producción con datos representativos de lo que va a utilizar.

Resultados

Las tablas de búsqueda tomaron la delantera a la manipulación de bytes. Básicamente, hay alguna forma de precomputar lo que cualquier nibble o byte dado estará en hexadecimal. Luego, a medida que revisas los datos, simplemente busca la siguiente porción para ver qué cadena hexagonal sería. Ese valor luego se agrega a la salida de cadena resultante de alguna manera. Durante mucho tiempo, la manipulación de bytes, potencialmente más difícil de leer por parte de algunos desarrolladores, fue el enfoque de mayor rendimiento.

Su mejor opción es encontrar algunos datos representativos y probarlos en un entorno de producción. Si tiene diferentes restricciones de memoria, puede preferir un método con menos asignaciones a uno que sea más rápido pero consuma más memoria.

Código de prueba

Siéntase libre de jugar con el código de prueba que utilicé. Se incluye una versión aquí, pero siéntase libre de clonar repo y agrega tus propios métodos. Envíe una solicitud de extracción si encuentra algo interesante o desea ayudar a mejorar el marco de prueba que utiliza.

  1. Agregue el nuevo método estático (Func<byte[], string>) a / Pruebas / ConvertByteArrayToHexString / Test.cs.
  2. Agregue el nombre de ese método al TestCandidates devolver el valor en esa misma clase.
  3. Asegúrese de que está ejecutando la versión de entrada que desea, frase o texto, al alternar los comentarios en GenerateTestInput en esa misma clase.
  4. Golpear F5 y espere la salida (también se genera un volcado de HTML en la carpeta / bin).
static string ByteArrayToHexStringViaStringJoinArrayConvertAll(byte[] bytes) {
    return string.Join(string.Empty, Array.ConvertAll(bytes, b => b.ToString("X2")));
}
static string ByteArrayToHexStringViaStringConcatArrayConvertAll(byte[] bytes) {
    return string.Concat(Array.ConvertAll(bytes, b => b.ToString("X2")));
}
static string ByteArrayToHexStringViaBitConverter(byte[] bytes) {
    string hex = BitConverter.ToString(bytes);
    return hex.Replace("-", "");
}
static string ByteArrayToHexStringViaStringBuilderAggregateByteToString(byte[] bytes) {
    return bytes.Aggregate(new StringBuilder(bytes.Length * 2), (sb, b) => sb.Append(b.ToString("X2"))).ToString();
}
static string ByteArrayToHexStringViaStringBuilderForEachByteToString(byte[] bytes) {
    StringBuilder hex = new StringBuilder(bytes.Length * 2);
    foreach (byte b in bytes)
        hex.Append(b.ToString("X2"));
    return hex.ToString();
}
static string ByteArrayToHexStringViaStringBuilderAggregateAppendFormat(byte[] bytes) {
    return bytes.Aggregate(new StringBuilder(bytes.Length * 2), (sb, b) => sb.AppendFormat("{0:X2}", b)).ToString();
}
static string ByteArrayToHexStringViaStringBuilderForEachAppendFormat(byte[] bytes) {
    StringBuilder hex = new StringBuilder(bytes.Length * 2);
    foreach (byte b in bytes)
        hex.AppendFormat("{0:X2}", b);
    return hex.ToString();
}
static string ByteArrayToHexViaByteManipulation(byte[] bytes) {
    char[] c = new char[bytes.Length * 2];
    byte b;
    for (int i = 0; i < bytes.Length; i++) {
        b = ((byte)(bytes[i] >> 4));
        c[i * 2] = (char)(b > 9 ? b + 0x37 : b + 0x30);
        b = ((byte)(bytes[i] & 0xF));
        c[i * 2 + 1] = (char)(b > 9 ? b + 0x37 : b + 0x30);
    }
    return new string(c);
}
static string ByteArrayToHexViaByteManipulation2(byte[] bytes) {
    char[] c = new char[bytes.Length * 2];
    int b;
    for (int i = 0; i < bytes.Length; i++) {
        b = bytes[i] >> 4;
        c[i * 2] = (char)(55 + b + (((b - 10) >> 31) & -7));
        b = bytes[i] & 0xF;
        c[i * 2 + 1] = (char)(55 + b + (((b - 10) >> 31) & -7));
    }
    return new string(c);
}
static string ByteArrayToHexViaSoapHexBinary(byte[] bytes) {
    SoapHexBinary soapHexBinary = new SoapHexBinary(bytes);
    return soapHexBinary.ToString();
}
static string ByteArrayToHexViaLookupAndShift(byte[] bytes) {
    StringBuilder result = new StringBuilder(bytes.Length * 2);
    string hexAlphabet = "0123456789ABCDEF";
    foreach (byte b in bytes) {
        result.Append(hexAlphabet[(int)(b >> 4)]);
        result.Append(hexAlphabet[(int)(b & 0xF)]);
    }
    return result.ToString();
}
static readonly uint* _lookup32UnsafeP = (uint*)GCHandle.Alloc(_Lookup32, GCHandleType.Pinned).AddrOfPinnedObject();
static string ByteArrayToHexViaLookup32UnsafeDirect(byte[] bytes) {
    var lookupP = _lookup32UnsafeP;
    var result = new string((char)0, bytes.Length * 2);
    fixed (byte* bytesP = bytes)
    fixed (char* resultP = result) {
        uint* resultP2 = (uint*)resultP;
        for (int i = 0; i < bytes.Length; i++) {
            resultP2[i] = lookupP[bytesP[i]];
        }
    }
    return result;
}
static uint[] _Lookup32 = Enumerable.Range(0, 255).Select(i => {
    string s = i.ToString("X2");
    return ((uint)s[0]) + ((uint)s[1] << 16);
}).ToArray();
static string ByteArrayToHexViaLookupPerByte(byte[] bytes) {
    var result = new char[bytes.Length * 2];
    for (int i = 0; i < bytes.Length; i++)
    {
        var val = _Lookup32[bytes[i]];
        result[2*i] = (char)val;
        result[2*i + 1] = (char) (val >> 16);
    }
    return new string(result);
}
static string ByteArrayToHexViaLookup(byte[] bytes) {
    string[] hexStringTable = new string[] {
        "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0A", "0B", "0C", "0D", "0E", "0F",
        "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1A", "1B", "1C", "1D", "1E", "1F",
        "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "2A", "2B", "2C", "2D", "2E", "2F",
        "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "3A", "3B", "3C", "3D", "3E", "3F",
        "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "4A", "4B", "4C", "4D", "4E", "4F",
        "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "5A", "5B", "5C", "5D", "5E", "5F",
        "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "6A", "6B", "6C", "6D", "6E", "6F",
        "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "7A", "7B", "7C", "7D", "7E", "7F",
        "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "8A", "8B", "8C", "8D", "8E", "8F",
        "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "9A", "9B", "9C", "9D", "9E", "9F",
        "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "AA", "AB", "AC", "AD", "AE", "AF",
        "B0", "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B9", "BA", "BB", "BC", "BD", "BE", "BF",
        "C0", "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "CA", "CB", "CC", "CD", "CE", "CF",
        "D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "DA", "DB", "DC", "DD", "DE", "DF",
        "E0", "E1", "E2", "E3", "E4", "E5", "E6", "E7", "E8", "E9", "EA", "EB", "EC", "ED", "EE", "EF",
        "F0", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "FA", "FB", "FC", "FD", "FE", "FF",
    };
    StringBuilder result = new StringBuilder(bytes.Length * 2);
    foreach (byte b in bytes) {
        result.Append(hexStringTable[b]);
    }
    return result.ToString();
}

Actualización (13-01-2010)

Se agregó la respuesta de Waleed al análisis. Bastante rapido.

Actualización (10-10-2010)

Adicional string.Concat  Array.ConvertAll variante para completar (requiere .NET 4.0). A la par de string.Join versión.

Actualización (2012-02-05)

El informe de prueba incluye más variantes, como StringBuilder.Append(b.ToString("X2")). Ninguno disgustó los resultados. foreach es más rápido que {IEnumerable}.Aggregate, por ejemplo, pero BitConverter aún gana

Actualización (2012-04-03)

Agregado de Mykroft SoapHexBinary respuesta al análisis, que ocupó el tercer lugar.

Actualización (2013-01-15)

Se agregó la respuesta de manipulación de byte de CodesInChaos, que ocupó el primer lugar (por un amplio margen en bloques de texto grandes).

Actualización (2013-05-23)

Se agregó la respuesta de búsqueda de Nathan Moinvaziri y la variante del blog de Brian Lambert. Ambos bastante rápidos, pero no tomaron la delantera en la máquina de prueba que utilicé (AMD Phenom 9750).

Actualización (2014-07-31)

Se agregó la nueva respuesta de búsqueda basada en byte de @ CodesInChaos. Parece que ha tomado la delantera tanto en las pruebas de oraciones como en las pruebas de texto completo.

Actualización (2015-08-20)

Adicional aireador optimizaciones y unsafe variante de esto Repo de la respuesta. Si quieres jugar en el juego inseguro, puedes obtener grandes ganancias de rendimiento sobre cualquiera de los principales ganadores anteriores tanto en cadenas cortas como en textos grandes.


405



Hay una clase llamada SoapHexBinary eso hace exactamente lo que quieres

using System.Runtime.Remoting.Metadata.W3cXsd2001;

public static byte[] GetStringToBytes(string value)
{
    SoapHexBinary shb = SoapHexBinary.Parse(value);
    return shb.Value;
}

public static string GetBytesToString(byte[] value)
{
    SoapHexBinary shb = new SoapHexBinary(value);
    return shb.ToString();
}

210



Al escribir código criptográfico, es común evitar las ramas dependientes de datos y las búsquedas de tabla para garantizar que el tiempo de ejecución no dependa de los datos, ya que el tiempo dependiente de los datos puede provocar ataques de canal lateral.

También es bastante rápido.

static string ByteToHexBitFiddle(byte[] bytes)
{
    char[] c = new char[bytes.Length * 2];
    int b;
    for (int i = 0; i < bytes.Length; i++) {
        b = bytes[i] >> 4;
        c[i * 2] = (char)(55 + b + (((b-10)>>31)&-7));
        b = bytes[i] & 0xF;
        c[i * 2 + 1] = (char)(55 + b + (((b-10)>>31)&-7));
    }
    return new string(c);
}

Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn


Abandona toda esperanza, tú que entren aquí

Una explicación del toque de bits extraño:

  1. bytes[i] >> 4 extrae el alto mordisco de un byte
    bytes[i] & 0xF extrae el mordisco bajo de un byte
  2. b - 10
    es < 0 para valores b < 10, que se convertirá en un dígito decimal
    es >= 0 para valores b > 10, que se convertirá en una carta de A a F.
  3. Utilizando i >> 31 en un entero con signo de 32 bits extrae el signo, gracias a la extensión de signo. Será -1 para i < 0 y 0 para i >= 0.
  4. Combinando 2) y 3), muestra que (b-10)>>31 estarán 0 para cartas y -1 para los dígitos.
  5. Mirando el caso de las letras, se convierte el último summand 0y b está en el rango de 10 a 15. Queremos asignarlo a A(65) a F(70), lo que implica agregar 55 ('A'-10)
  6. Mirando el caso de los dígitos, queremos adaptar el último summand para que mapas b del rango de 0 a 9 al rango 0(48) a 9(57) Esto significa que necesita convertirse en -7 ('0' - 55)
    Ahora podríamos simplemente multiplicar por 7. Pero como -1 está representado por todos los bits siendo 1, podemos usar & -7 ya que (0 & -7) == 0 y (-1 & -7) == -7.

Algunas consideraciones adicionales:

  • No usé una segunda variable de bucle para indexar en c, ya que la medición muestra que se calcula desde i es más barato.
  • Usando exactamente i < bytes.Length como límite superior del ciclo permite que JITter elimine las verificaciones de límites en bytes[i]así que elegí esa variante.
  • Fabricación b un int permite conversiones innecesarias desde y hacia bytes.

123



Si quieres más flexibilidad que BitConverter, pero no quieres esos logos explícitos al estilo de los 1990, entonces puedes hacer:

String.Join(String.Empty, Array.ConvertAll(bytes, x => x.ToString("X2")));

O bien, si usa .NET 4.0:

String.Concat(Array.ConvertAll(bytes, x => x.ToString("X2")));

(El último de un comentario en la publicación original).


83



Puede usar el método BitConverter.ToString:

byte[] bytes = {0, 1, 2, 4, 8, 16, 32, 64, 128, 256}
Console.WriteLine( BitConverter.ToString(bytes));

Salida:

00-01-02-04-08-10-20-40-80-FF

Más información: Método BitConverter.ToString (Byte [])


56



Otro enfoque basado en la tabla de búsqueda. Este usa solo una tabla de búsqueda para cada byte, en lugar de una tabla de búsqueda por mordisco.

private static readonly uint[] _lookup32 = CreateLookup32();

private static uint[] CreateLookup32()
{
    var result = new uint[256];
    for (int i = 0; i < 256; i++)
    {
        string s=i.ToString("X2");
        result[i] = ((uint)s[0]) + ((uint)s[1] << 16);
    }
    return result;
}

private static string ByteArrayToHexViaLookup32(byte[] bytes)
{
    var lookup32 = _lookup32;
    var result = new char[bytes.Length * 2];
    for (int i = 0; i < bytes.Length; i++)
    {
        var val = lookup32[bytes[i]];
        result[2*i] = (char)val;
        result[2*i + 1] = (char) (val >> 16);
    }
    return new string(result);
}

También probé variantes de esto usando ushort, struct{char X1, X2}, struct{byte X1, X2} en la tabla de búsqueda.

Dependiendo del objetivo de compilación (x86, X64), estos tenían aproximadamente el mismo rendimiento o eran ligeramente más lentos que esta variante.


Y para un rendimiento aún mayor, es unsafe hermano:

private static readonly uint[] _lookup32Unsafe = CreateLookup32Unsafe();
private static readonly uint* _lookup32UnsafeP = (uint*)GCHandle.Alloc(_lookup32Unsafe,GCHandleType.Pinned).AddrOfPinnedObject();

private static uint[] CreateLookup32Unsafe()
{
    var result = new uint[256];
    for (int i = 0; i < 256; i++)
    {
        string s=i.ToString("X2");
        if(BitConverter.IsLittleEndian)
            result[i] = ((uint)s[0]) + ((uint)s[1] << 16);
        else
            result[i] = ((uint)s[1]) + ((uint)s[0] << 16);
    }
    return result;
}

public static string ByteArrayToHexViaLookup32Unsafe(byte[] bytes)
{
    var lookupP = _lookup32UnsafeP;
    var result = new char[bytes.Length * 2];
    fixed(byte* bytesP = bytes)
    fixed (char* resultP = result)
    {
        uint* resultP2 = (uint*)resultP;
        for (int i = 0; i < bytes.Length; i++)
        {
            resultP2[i] = lookupP[bytesP[i]];
        }
    }
    return new string(result);
}

O si considera aceptable escribir directamente en la cadena:

public static string ByteArrayToHexViaLookup32UnsafeDirect(byte[] bytes)
{
    var lookupP = _lookup32UnsafeP;
    var result = new string((char)0, bytes.Length * 2);
    fixed (byte* bytesP = bytes)
    fixed (char* resultP = result)
    {
        uint* resultP2 = (uint*)resultP;
        for (int i = 0; i < bytes.Length; i++)
        {
            resultP2[i] = lookupP[bytesP[i]];
        }
    }
    return result;
}

53



Me acabo de encontrar el mismo problema hoy, y me encontré con este código:

private static string ByteArrayToHex(byte[] barray)
{
    char[] c = new char[barray.Length * 2];
    byte b;
    for (int i = 0; i < barray.Length; ++i)
    {
        b = ((byte)(barray[i] >> 4));
        c[i * 2] = (char)(b > 9 ? b + 0x37 : b + 0x30);
        b = ((byte)(barray[i] & 0xF));
        c[i * 2 + 1] = (char)(b > 9 ? b + 0x37 : b + 0x30);
    }
    return new string(c);
}

Fuente: Publicación del Foro byte [] Matriz a cadena hexagonal (ver la publicación de PZahra). Modifiqué un poco el código para eliminar el prefijo 0x.

Hice algunas pruebas de rendimiento para el código y fue casi ocho veces más rápido que usar BitConverter.ToString () (el más rápido según la publicación de patridge).


52



Este problema también podría resolverse usando una tabla de búsqueda. Esto requeriría una pequeña cantidad de memoria estática tanto para el codificador como para el decodificador. Sin embargo, este método será rápido:

  • Tabla de codificador de 512 bytes o 1024 bytes (dos veces el tamaño si ambas mayúsculas y minúsculas es necesario)
  • Tabla de decodificador de 256 bytes o 64 KiB (ya sea una sola búsqueda de carbón) o búsqueda de doble char)

Mi solución usa 1024 bytes para la tabla de codificación y 256 bytes para la decodificación.

Descodificación

private static readonly byte[] LookupTable = new byte[] {
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};

private static byte Lookup(char c)
{
  var b = LookupTable[c];
  if (b == 255)
    throw new IOException("Expected a hex character, got " + c);
  return b;
}

public static byte ToByte(char[] chars, int offset)
{
  return (byte)(Lookup(chars[offset]) << 4 | Lookup(chars[offset + 1]));
}

Codificación

private static readonly char[][] LookupTableUpper;
private static readonly char[][] LookupTableLower;

static Hex()
{
  LookupTableLower = new char[256][];
  LookupTableUpper = new char[256][];
  for (var i = 0; i < 256; i++)
  {
    LookupTableLower[i] = i.ToString("x2").ToCharArray();
    LookupTableUpper[i] = i.ToString("X2").ToCharArray();
  }
}

public static char[] ToCharLower(byte[] b, int bOffset)
{
  return LookupTableLower[b[bOffset]];
}

public static char[] ToCharUpper(byte[] b, int bOffset)
{
  return LookupTableUpper[b[bOffset]];
}

Comparación

StringBuilderToStringFromBytes:   106148
BitConverterToStringFromBytes:     15783
ArrayConvertAllToStringFromBytes:  54290
ByteManipulationToCharArray:        8444
TableBasedToCharArray:              5651 *

* esta solución

Nota

Durante la decodificación, pueden ocurrir IOException y IndexOutOfRangeException (si un carácter tiene un valor demasiado alto> 256). Deben implementarse métodos para de / encoding streams o arrays, esto es solo una prueba de concepto.


15



Esta es una respuesta a revisión 4 de La respuesta muy popular de Tomalak (y ediciones posteriores).

Presentaré el caso de que esta edición es incorrecta y explicaré por qué podría revertirse. En el camino, puede aprender una o dos cosas sobre algunos aspectos internos, y ver otro ejemplo de lo que es la optimización prematura y cómo puede morderlo.

tl; dr: Solo usa Convert.ToByte y String.Substring si tiene prisa ("Código original" a continuación), es la mejor combinación si no desea volver a implementar Convert.ToByte. Use algo más avanzado (ver otras respuestas) que no use Convert.ToByte si tu necesitar actuación. Hacer no usar cualquier otra cosa que no sea String.Substring en combinación con Convert.ToByte, a menos que alguien tenga algo interesante que decir al respecto en los comentarios de esta respuesta.

advertencia: Esta respuesta puede quedar obsoleta Si un Convert.ToByte(char[], Int32) la sobrecarga se implementa en el marco. Es poco probable que esto suceda pronto.

Como regla general, no me gusta decir "no optimizar prematuramente", porque nadie sabe cuándo es "prematuro". Lo único que debe considerar al decidir si optimizar o no es: "¿Tengo el tiempo y los recursos para investigar los enfoques de optimización de forma adecuada?". Si no lo hace, entonces es demasiado pronto, espere hasta que su proyecto esté más maduro o hasta que necesite el rendimiento (si hay una necesidad real, entonces lo hará). hacer el tiempo). Mientras tanto, haz lo más simple que pueda funcionar en su lugar.

Código original:

    public static byte[] HexadecimalStringToByteArray_Original(string input)
    {
        var outputLength = input.Length / 2;
        var output = new byte[outputLength];
        for (var i = 0; i < outputLength; i++)
            output[i] = Convert.ToByte(input.Substring(i * 2, 2), 16);
        return output;
    }

Revisión 4:

    public static byte[] HexadecimalStringToByteArray_Rev4(string input)
    {
        var outputLength = input.Length / 2;
        var output = new byte[outputLength];
        using (var sr = new StringReader(input))
        {
            for (var i = 0; i < outputLength; i++)
                output[i] = Convert.ToByte(new string(new char[2] { (char)sr.Read(), (char)sr.Read() }), 16);
        }
        return output;
    }

La revisión evita String.Substring y usa un StringReader en lugar. La razón dada es:

Editar: puede mejorar el rendimiento de cadenas largas usando un solo   pasar el analizador, así:

Bueno, mirando el código de referencia para String.Substring, es claramente "single-pass" ya; y por qué no debería ser? Funciona a nivel de byte, no a pares de sustitución.

Sin embargo, asigna una nueva cadena, pero luego debe asignar una para pasar a Convert.ToByte de todas formas. Además, la solución provista en la revisión asigna otro objeto en cada iteración (la matriz de dos caracteres); puede colocar esa asignación de forma segura fuera del ciclo y reutilizar la matriz para evitar eso.

    public static byte[] HexadecimalStringToByteArray(string input)
    {
        var outputLength = input.Length / 2;
        var output = new byte[outputLength];
        var numeral = new char[2];
        using (var sr = new StringReader(input))
        {
            for (var i = 0; i < outputLength; i++)
            {
                numeral[0] = (char)sr.Read();
                numeral[1] = (char)sr.Read();
                output[i] = Convert.ToByte(new string(numeral), 16);
            }
        }
        return output;
    }

Cada hexadecimal numeral representa un solo octeto usando dos dígitos (símbolos).

Pero entonces, ¿por qué llamar StringReader.Read ¿dos veces? Simplemente llame a su segunda sobrecarga y pídale que lea dos caracteres en la matriz de dos caracteres a la vez; y reducir la cantidad de llamadas por dos.

    public static byte[] HexadecimalStringToByteArray(string input)
    {
        var outputLength = input.Length / 2;
        var output = new byte[outputLength];
        var numeral = new char[2];
        using (var sr = new StringReader(input))
        {
            for (var i = 0; i < outputLength; i++)
            {
                var read = sr.Read(numeral, 0, 2);
                Debug.Assert(read == 2);
                output[i] = Convert.ToByte(new string(numeral), 16);
            }
        }
        return output;
    }

Lo que te queda es un lector de cuerdas cuyo único "valor" agregado es un índice paralelo (interno _pos) que podrías haber declarado tú mismo (como j por ejemplo), una variable de longitud redundante (interna _length), y una referencia redundante a la cadena de entrada (interna _s) En otras palabras, es inútil.

Si te preguntas cómo Read "lee", solo mira el código, todo lo que hace es llamar String.CopyTo en la cadena de entrada. El resto es solo una sobrecarga de contabilidad para mantener los valores que no necesitamos.

Por lo tanto, elimine el lector de cadenas ya, y llame CopyTo tú mismo; es más simple, más claro y más eficiente.

    public static byte[] HexadecimalStringToByteArray(string input)
    {
        var outputLength = input.Length / 2;
        var output = new byte[outputLength];
        var numeral = new char[2];
        for (int i = 0, j = 0; i < outputLength; i++, j += 2)
        {
            input.CopyTo(j, numeral, 0, 2);
            output[i] = Convert.ToByte(new string(numeral), 16);
        }
        return output;
    }

¿Realmente necesitas un j índice que se incrementa en pasos de dos paralelos a i? Por supuesto que no, solo multiplique i por dos (que el compilador debería poder optimizar a una adición).

    public static byte[] HexadecimalStringToByteArray_BestEffort(string input)
    {
        var outputLength = input.Length / 2;
        var output = new byte[outputLength];
        var numeral = new char[2];
        for (int i = 0; i < outputLength; i++)
        {
            input.CopyTo(i * 2, numeral, 0, 2);
            output[i] = Convert.ToByte(new string(numeral), 16);
        }
        return output;
    }

¿Cómo se ve la solución ahora? Exactamente como fue al principio, solo que en lugar de usar String.Substring para asignar la cadena y copiar los datos en ella, está utilizando una matriz intermedia a la que copia los números hexadecimales, luego asigne la cadena usted mismo y copie los datos de nuevo de la matriz y en la cadena (cuando lo pasa en el constructor de cadenas). La segunda copia podría estar optimizada, si la cadena ya está en el grupo interno, pero luego String.Substring también podrá evitarlo en estos casos.

De hecho, si miras String.Substring de nuevo, ve que usa algún conocimiento interno de bajo nivel sobre cómo se construyen las cadenas para asignar la cadena más rápido de lo que normalmente podría hacerlo, e inserta el mismo código utilizado por CopyTo directamente allí para evitar la sobrecarga de llamadas.

String.Substring

  • Peor caso: una asignación rápida, una copia rápida.
  • Mejor caso: sin asignación, sin copia.

Método manual

  • Peor caso: dos asignaciones normales, una copia normal, una copia rápida.
  • Mejor caso: una asignación normal, una copia normal.

¿Conclusión? Si quieres usar Convert.ToByte(String, Int32) (porque no desea volver a implementar esa funcionalidad usted mismo), no parece haber una manera de vencer String.Substring; todo lo que haces es correr en círculos, reinventar la rueda (solo con materiales subóptimos).

Tenga en cuenta que el uso Convert.ToByte y String.Substring es una elección perfectamente válida si no necesita un rendimiento extremo. Recuerde: solo opte por una alternativa si tiene el tiempo y los recursos para investigar cómo funciona correctamente.

Si hubiera un Convert.ToByte(char[], Int32), las cosas serían diferentes por supuesto (sería posible hacer lo que describí anteriormente y evitar por completo String)

Sospecho que las personas que reportan un mejor desempeño al "evitar String.Substring"también evitar Convert.ToByte(String, Int32), lo que en realidad debería hacer si necesita el rendimiento de todos modos. Mire las innumerables otras respuestas para descubrir todos los diferentes enfoques para hacer eso.

Descargo de responsabilidad: no he descompilado la última versión del marco para verificar que la fuente de referencia esté actualizada, supongo que sí.

Ahora, todo suena bien y es lógico, con suerte incluso obvio si has logrado llegar tan lejos. ¿Pero es verdad?

Intel(R) Core(TM) i7-3720QM CPU @ 2.60GHz
    Cores: 8
    Current Clock Speed: 2600
    Max Clock Speed: 2600
--------------------
Parsing hexadecimal string into an array of bytes
--------------------
HexadecimalStringToByteArray_Original: 7,777.09 average ticks (over 10000 runs), 1.2X
HexadecimalStringToByteArray_BestEffort: 8,550.82 average ticks (over 10000 runs), 1.1X
HexadecimalStringToByteArray_Rev4: 9,218.03 average ticks (over 10000 runs), 1.0X

¡Sí!

Apoyos a Partridge para el marco de referencia, es fácil de hackear. La entrada utilizada es el siguiente hash SHA-1 repetido 5000 veces para hacer una cadena de 100.000 bytes de longitud.

209113288F93A9AB8E474EA78D899AFDBB874355

¡Que te diviertas! (Pero optimice con moderación)


14