Вопрос производительности: самый быстрый способ преобразовать шестнадцатеричный символ в его числовое значение в Java?

Я хочу преобразовать из char, представляющего шестнадцатеричное значение (в верхнем или нижнем регистре), в байт, например

'0'->0, '1' -> 1, 'A' -> 10, 'a' -> 10, 'f' -> 15 etc...

Я буду вызывать этот метод очень часто, поэтому важна производительность. Есть ли более быстрый способ, чем использовать предварительно инициализированный HashMap<Character,Byte> для получения значения?

Ответить

Кажется, что это выбор между использованием Switch-Case и решением Джона Скита для прямых вычислений - однако решение Switch-Case, похоже, немного отстает. Метод массива Грега побеждает. Вот результаты производительности (в мс) для 200 000 000 запусков различных методов:

Character.getNumericValue:
8360

Character.digit:
8453

HashMap<Character,Byte>:
15109

Greg's Array Method:
6656

JonSkeet's Direct Method:
7344

Switch:
7281

Спасибо, парни!

Код метода сравнения

Держи, ДжонСкит, ты, старый конкурент. ;-)

public class ScratchPad {

    private static final int NUMBER_OF_RUNS = 200000000;

    static byte res;

    static HashMap<Character, Byte> map = new HashMap<Character, Byte>() {{
        put( Character.valueOf( '0' ), Byte.valueOf( (byte )0 ));
        put( Character.valueOf( '1' ), Byte.valueOf( (byte )1 ));
        put( Character.valueOf( '2' ), Byte.valueOf( (byte )2 ));
        put( Character.valueOf( '3' ), Byte.valueOf( (byte )3 ));
        put( Character.valueOf( '4' ), Byte.valueOf( (byte )4 ));
        put( Character.valueOf( '5' ), Byte.valueOf( (byte )5 ));
        put( Character.valueOf( '6' ), Byte.valueOf( (byte )6 ));
        put( Character.valueOf( '7' ), Byte.valueOf( (byte )7 ));
        put( Character.valueOf( '8' ), Byte.valueOf( (byte )8 ));
        put( Character.valueOf( '9' ), Byte.valueOf( (byte )9 ));
        put( Character.valueOf( 'a' ), Byte.valueOf( (byte )10 ));
        put( Character.valueOf( 'b' ), Byte.valueOf( (byte )11 ));
        put( Character.valueOf( 'c' ), Byte.valueOf( (byte )12 ));
        put( Character.valueOf( 'd' ), Byte.valueOf( (byte )13 ));
        put( Character.valueOf( 'e' ), Byte.valueOf( (byte )14 ));
        put( Character.valueOf( 'f' ), Byte.valueOf( (byte )15 ));
        put( Character.valueOf( 'A' ), Byte.valueOf( (byte )10 ));
        put( Character.valueOf( 'B' ), Byte.valueOf( (byte )11 ));
        put( Character.valueOf( 'C' ), Byte.valueOf( (byte )12 ));
        put( Character.valueOf( 'D' ), Byte.valueOf( (byte )13 ));
        put( Character.valueOf( 'E' ), Byte.valueOf( (byte )14 ));
        put( Character.valueOf( 'F' ), Byte.valueOf( (byte )15 ));
    }};
    static int[] charValues = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13,14,15,-1,-1,-1,-1,-1,-1,-1,-1,-1,
                    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,10, 11, 12, 13,14,15};
    static char[] cs = new char[]{'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','A','B','C','D','E','F'};

    public static void main(String args[]) throws Exception {
        long time = System.currentTimeMillis();
        for( int i = 0; i < NUMBER_OF_RUNS; i++ ) {
            res = getNumericValue( i );
        }
        System.out.println( "Character.getNumericValue:" );
        System.out.println( System.currentTimeMillis()-time );
        time = System.currentTimeMillis();
        for( int i = 0; i < NUMBER_OF_RUNS; i++ ) {
            res = getDigit( i );
        }
        System.out.println( "Character.digit:" );
        System.out.println( System.currentTimeMillis()-time );
        time = System.currentTimeMillis();
        for( int i = 0; i < NUMBER_OF_RUNS; i++ ) {
            try {
                res = getValueFromArray( i );
            } catch (IllegalArgumentException e) {
            }
        }
        System.out.println( "Array:" );
        System.out.println( System.currentTimeMillis()-time );
        time = System.currentTimeMillis();
        for( int i = 0; i < NUMBER_OF_RUNS; i++ ) {
            res = getValueFromHashMap( i );
        }
        System.out.println( "HashMap<Character,Byte>:" );
        System.out.println( System.currentTimeMillis()-time );
        time = System.currentTimeMillis();
        for( int i = 0; i < NUMBER_OF_RUNS; i++ ) {
            char c = cs[i%cs.length];
            res = getValueFromComputeMethod( c );        
        }
        System.out.println( "JonSkeet's Direct Method:" );
        System.out.println( System.currentTimeMillis()-time );
        time = System.currentTimeMillis();
        for( int i = 0; i < NUMBER_OF_RUNS; i++ ) {
            res = getValueFromSwitch( i );

        }
        System.out.println( "Switch:" );
        System.out.println( System.currentTimeMillis()-time );
    }

    private static byte getValueFromSwitch( int i ) {
        byte res;
        char ch = cs[i%cs.length];
        switch( ch ) {
            case '0':
                res = 0;
                break;
            case '1':
                res = 1;
                break;
            case '2':
                res = 2;
                break;
            case '3':
                res = 3;
                break;
            case '4':
                res = 4;
                break;
            case '5':
                res = 5;
                break;
            case '6':
                res = 6;
                break;
            case '7':
                res = 7;
                break;
            case '8':
                res = 8;
                break;
            case '9':
                res = 9;
                break;
            case 'a':
            case 'A':
                res = 10;
                break;
            case 'b':
            case 'B':    
                res = 11;
                break;
            case 'c':
            case 'C':    
                res = 12;
                break;
            case 'd':
            case 'D':    
                res = 13;
                break;
            case 'e':
            case 'E':    
                res = 14;
                break;
            case 'f':
            case 'F':    
                res = 15;
                break;
            default:
                throw new RuntimeException("unknown hex character: " + ch );
        }
        return res;
    }

    private static byte getValueFromComputeMethod( char c ) {
        byte result = 0;
        if (c >= '0' && c <= '9')
        {
            result =  (byte)(c - '0');
        }
        if (c >= 'a' && c <= 'f')
        {
            result = (byte)(c - 'a' + 10);
        }
        if (c >= 'A' && c <= 'F')
        {
            result =  (byte)(c - 'A' + 10);
        }
        return result;
    }

    private static byte getValueFromHashMap( int i ) {
        return map.get( Character.valueOf( cs[i%cs.length] ) ).byteValue();
    }

    private static byte getValueFromArray( int i ) {
        char c = cs[i%cs.length];
        if (c < '0' || c > 'f') {
            throw new IllegalArgumentException();
        }
        byte result = (byte)charValues[c-'0'];
        if (res < 0) {
            throw new IllegalArgumentException();
        }
        return result;
    }

    private static byte getDigit( int i ) {
        return (byte)Character.digit( cs[i%cs.length], 16 );
    }

    private static byte getNumericValue( int i ) {
        return (byte)Character.getNumericValue( cs[i%cs.length] );
    }

}

person Epaga    schedule 21.10.2008    source источник
comment
Были ли проблемы с производительностью при использовании библиотечных функций? Если их нет, должно быть достаточно Character.digit(char, radix).   -  person questzen    schedule 21.10.2008
comment
Одна вещь, которую следует учитывать при использовании этого эталонного теста, заключается в том, что массив будет работать хорошо, пока он находится в кеше, что отлично, если вы выполняете много итераций за один раз. Однако, если вам нужно дождаться извлечения массива из основной памяти, чисто вычислительный подход будет быстрее.   -  person Jon Skeet    schedule 21.10.2008
comment
Другими словами: это зависит :)   -  person Jon Skeet    schedule 21.10.2008
comment
Эпага: Не могли бы вы опубликовать свой тестовый код? У меня есть идея немного подправить код Грега, что может сделать его немного быстрее. Я бы предпочел не изобретать велосипед, написав свой собственный тест :)   -  person Jon Skeet    schedule 21.10.2008


Ответы (13)


Предварительно инициализированный массив будет быстрее, чем HashMap. Что-то вроде этого:

int CharValues['f'-'0'+1] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, ... -1, 10, 11, 12, ...};

if (c < '0' || c > 'f') {
    throw new IllegalArgumentException();
}
int n = CharValues[c-'0'];
if (n < 0) {
    throw new IllegalArgumentException();
}
// n contains the digit value

Вы должны сравнить этот метод с другими методами (например, value#221012">прямой метод Джона Скита), чтобы определить, какой из них будет самым быстрым для вашего приложения.

person Greg Hewgill    schedule 21.10.2008
comment
первую проверку на диапазон нужно убрать: она только замедляет конвертацию; массив сам проверит свои границы; исключение будет другим, но мы торгуем всем ради скорости, не так ли? - person Vladimir Dyuzhev; 21.10.2008
comment
Это хорошая идея, и если вас беспокоит тип исключения, вы можете перехватить исключение массива и сгенерировать другое. - person Greg Hewgill; 21.10.2008

Хеш-таблица будет относительно медленной. Это довольно быстро:

if (c >= '0' && c <= '9')
{
    return c - '0';
}
if (c >= 'a' && c <= 'f')
{
    return c - 'a' + 10;
}
if (c >= 'A' && c <= 'F')
{
    return c - 'A' + 10;
}
throw new IllegalArgumentException();

Другой вариант - попробовать оператор switch/case. Массив может быть в порядке, если он находится в кеше, но промах может дорого обойтись.

person Jon Skeet    schedule 21.10.2008
comment
То, что я говорю трижды, верно. - person Michael Burr; 21.10.2008
comment
Ик, понятия не имею, что там произошло. Вот что я получаю за публикацию с мобильного. Будет редактировать. - person Jon Skeet; 21.10.2008
comment
Есть несколько вариантов этого, и, как правило, это будет самым быстрым, за исключением 256-байтовой таблицы поиска в стиле Pro. - person Hot Licks; 27.07.2013

Я не помню, чтобы видел этот метод раньше, но Микко Рантанен указал на это уравнение в комментарии к вопросу, Code golf - преобразование шестнадцатеричного кода в (необработанный) двоичный код

(char | 32) % 39 - 9

Я не знаю, как это будет оцениваться (возможно, кто-то может добавить его в приведенный выше тест и запустить его, но я предполагаю, что % убивает производительность) - но это аккуратный, простой однострочный шестнадцатеричный символ для десятичное преобразование. Ручки 0-9, A-F, a-f.

person Adam Davis    schedule 28.04.2009
comment
Модуль убивает производительность. Тем не менее, это было бы самым быстрым решением, если вы компилируете java в исполняемый файл, потому что на многих современных компьютерах скорость доступа к ОЗУ немного медленнее, из-за чего таблицы поиска действительно снижают производительность, если вы обращаетесь к ОЗУ для получения шестнадцатеричного числа. вы работаете с. - person Jack Giffin; 15.01.2017

Использование массива должно быть самым быстрым.

Массив может иметь размер 16, 16^2, 16^3, 16^4 и т.д..

Преобразование числа в более крупные группы даст увеличение производительности.

Там будет сладкое место, где это наиболее полезно, возможно, 4 цифры (таблица 64k).

person pro    schedule 21.10.2008
comment
Почему вы говорите, что это должно быть быстрее? Все зависит от баланса доступа к памяти и вычислений. Это будет частично зависеть от кеширования, архитектуры памяти вашей конкретной машины и т. д. Есть над чем подумать :) - person Jon Skeet; 21.10.2008
comment
Да, но вычисления включают доступ к памяти для инструкций. В случае одного символа он будет близок, как и в тестах, но даже в этом случае поиск в массиве - это только одно чтение. В общем случае выполнение блоков по 4 при поиске в массиве должно быть намного быстрее. Однако ваши очки отмечены! - person pro; 02.01.2009
comment
Использование шаблона бинарного поиска потребовало бы 16 итераций с таблицей размером 64 КБ и длиной 128 КБ, что делает его менее эффективным, чем решение Адама Дэвиса на большинстве современных аппаратных средств. - person Jack Giffin; 15.01.2017

Чувак, я программист микроконтроллеров, и в этом крошечном мире скорость действительно имеет значение. Самый быстрый способ преобразовать цифру «ASCII» в байт (из «A» в 0x0A) — использовать этот небольшой фрагмент кода.

c|=0x20;
return c<='9'? c+0xD0 : c+0xA9;

Магия этой команды проста: если вы посмотрите таблицу ascii, вы найдете числа, начинающиеся с 0x3_, и буквы в столбцах 0x4_ и 0x6_ соответственно. 1) если вы «или» входящий символ (переменная «c») с 0x20, вы никогда не будете менять числа... но для букв вы будете преобразовывать верхний регистр в нижний. Так как мы ищем только значения A..F(a..f)... Мне все равно, что другие. 2) теперь магия. Чтобы избежать вычитаний, я использую аддитивные операторы. 0xD0 совпадает с -0x30, а 0xA9 совпадает с -'a'+10;

инструкция ветвления, созданная при сравнении, довольно проста и не требует накладных расходов на поиск в таблице, ни на «мод», ни на другие операторы!

Наслаждаться...

person mauricio_martins    schedule 27.07.2013
comment
Это не работает для 16-битных символов Java, но я полагаю, что это можно адаптировать. - person Marko Topolnik; 27.07.2013

Character.getNumericValue(char) - это еще один способ:

char c = 'a';
System.out.println(c + "->" + Character.getNumericValue(c));

Например, печатает «a-> 10», как вы хотите. Кто-то еще должен был бы прокомментировать эффективность вызова статического метода по сравнению с поиском HashMap, или вы могли бы проверить это сами. Хотя мне он кажется чище/более читаемым.

person Keeg    schedule 21.10.2008

просто, но медленно:

int i = Integer.parseInt(String.ValueOf(c), 16);

Быстрее:

int i = Character.digit(c, 16);

Я не буду использовать какой-либо специальный код для «проблем с производительностью». Если вы действительно часто используете это, JIT создаст скомпилированный код, и выполнение станет быстрым. Держите свой код в чистоте. Вы можете попробовать написать тест производительности, сравнивая время выполнения из приведенного выше кода и любой специальной реализации - держу пари, вы не получите значительных улучшений.

person Arne Burmeister    schedule 21.10.2008

Я не думаю, что вы можете победить прямой поиск массива.

static final int[] precalc = new int['f'+1];
static {
    for (char c='0'; c<='9'; c++) precalc[c] = c-'0';
    for (char c='A'; c<='F'; c++) precalc[c] = c-'A';
    for (char c='a'; c<='f'; c++) precalc[c] = c-'a';
}

System.out.println(precalc['f']);
person Staale    schedule 21.10.2008

Вот моя измененная версия кода Грега. На моей машине это незначительно быстрее, но, вероятно, в пределах шума. Это позволяет избежать проверки нижней границы и не требует никакого вычитания. Создание массива 64 КБ и избегание проверки привязки любого, казалось, замедляло работу, но опять же, с таким временем практически невозможно быть уверенным, что реально, а что шум.

public class HexParser
{
    private static final byte VALUES = new int['f'];

    // Easier to get right for bozos like me (Jon) than
    // a hard-coded array :)
    static
    {
        for (int i=0; i < VALUES.length; i++)
        {
            VALUES[i] = (byte) -1;
        }
        for (int i='0'; i <= '9'; i++)
        {
            VALUES[i] = (byte) i-'0';
        }
        for (int i='A'; i <= 'F'; i++)
        {
            VALUES[i] = (byte) (i-'A'+10);
        }
        for (int i='a'; i <= 'f'; i++)
        {
            VALUES[i] = (byte) (i-'a'+10);
        }
    }

    public static byte parseHexChar(char c)
    {
        if (c > 'f')
        {
            throw new IllegalArgumentException();
        }
        byte ret = VALUES[c];
        if (ret == -1)
        {
            throw new IllegalArgumentException();
        }
        return ret;
    }
}
person Jon Skeet    schedule 21.10.2008

Стоит отметить, что в большинстве ваших тестов вы действительно рассчитываете время операции %. Эта операция занимает примерно столько же времени, сколько и некоторые другие варианты.

private static byte lookUpTest(int i) {
    return (byte) cs[i%cs.length];
}
person Peter Lawrey    schedule 28.04.2009
comment
полностью согласен. % - очень медленная операция. - person njzk2; 06.05.2013

Таблица 16-битных значений, в которой вы можете искать две цифры за раз, должна быть быстрее, чем массивы 8-битных значений, предложенные в других ответах. Вы будете перебирать данные в два раза быстрее, обращаться к массиву в два раза реже и получать доступ к шортам вместо байтов, и все это дает процессору меньше работы и больше возможностей.

8-битные значения будут предварительно вычислены как x[Integer.toHexString(i).charAt[0]] = i для 0 <= i < 256. Затем вы просто ищете x[c] для каждого символа c в шестнадцатеричной строке. Возможно, вы захотите дублировать записи для вариантов каждого шестнадцатеричного значения как в верхнем, так и в нижнем регистре.

Таблица 32-битных значений должна быть еще быстрее, в зависимости от того, сколько места вы можете себе позволить.

person user207421    schedule 27.07.2013

Согласно моим таймингам, следующий метод превосходит и без того довольно эффективный метод Джона Скита.

Идея состоит в том, чтобы воспользоваться тестом диапазона «A»... «F», чтобы также отбросить один из диапазонов «0»... «9» или «a»... «f».

public static int hex2Dig2(char c) {
    if (c < 'A') {
        if (c >= '0' && c <= '9')
            return c - '0';
    } else if (c > 'F') {
        if (c >= 'a' && c <= 'f')
            return c - 'a' + 10;
    } else {
        return c - 'A' + 10;
    }
    return -1; // or throw exception
}
person Florian F    schedule 01.07.2019

person    schedule
comment
Я считаю, что ваш массив неверен - «0» должен начинаться с адреса 0x30, у вас он начинается с 0x2D. «A» должен начинаться с 0x41, у вас он начинается с 0x40, и вы не поддерживаете символы нижнего регистра («a» начинается с 0x61) - person Adam Davis; 28.04.2009
comment
согласен - я думаю, что получил это прямо сейчас ... но был бы признателен, если бы вы проверили, поскольку у меня кружится голова, глядя на это - person pro; 05.05.2009