LINUX.ORG.RU

быстрый парсинг целочисленных значений

 


3

10

https://github.com/dzidzitop/libafc/blob/master/src/afc/number.h#L264

Сабж. Работает в 2-3 раза быстрее std::stroul от GCC. Pure C такую скорость выдать не сможет никогда - в этом сила «плюсов». У кого есть идеи что можно ускорить - предлагайте.

★★

Последнее исправление: dzidzitop (всего исправлений: 1)
Ответ на: комментарий от dzidzitop

блин, а я-то в кернеле колупаюсь, и никак не могу понять, что же ещё выжимать можно из кода железа.
у тебя пока нет даже понимания оценки качества того «выжимания». ты не различаешь скорость написания кода и скорость работы бинарника, из него полученного. так что плюсы плюсов я тебе объяснить не могу. они в другой плоскости находятся, не там, где из кода что-то выжимают.

Iron_Bug ★★★★★
()

Не удивит, если в итоге выяснится, что скриптовые языки, javascript в частности - выполняют это еще быстрее.

anonymous
()
Ответ на: комментарий от andreyu

Да запросто

uint32_t parse_32(char *str)
{
  uint32_t a;
  memcpy( &a, str, 4);


  a = (a & 0x0F0F0F0F) * 2561 >> 8;
  a = (a & 0x00FF00FF) * 6553601 >> 16;
  return a;
}

SZT ★★★★★
()

Работает в 2-3 раза быстрее std::stroul от GCC

Ну давай прикинем где именно ты выиграл:

  • умножение на константу делается через сдвиг-и-сложение, что несколько быстрее «абстрактного» умножения
  • ты считаешь, что очень четко и дерзко проверяешь валидность входных чисел

А теперь давай посмотрим, где проиграл:

  • для парсинга из char* и const char* будут сгенерированы две разные функции, а это удар по кешу инструкций
  • разбор знакового числа сделан максимально шизануто: вместо того, чтобы запомнить знак и сделать neg в конце, нагорожен полигон с бранчем — авось компилятор как-нибудь разберется
  • хочешь позвать parseNumber(char *, const char *)? А вот хрен тебе — давай делай никому не нужные касты или наблюдай неиллюзорную портянку шаблонной байды
  • определять основание в рантайме? Пара мегабайт разворачивания шаблонов и неиллюзорный switch на все допустимые варианты оснований идут в комплекте — наслаждайтесь!
  • ты считаешь, что очень четко и дерзко проверяешь валидность входных чисел; на деле «через массив из 256 элементов» val[цифра] = число будет быстрее и компактнее с учетом мегабайт шаблонного говна, которые ты наворотил
  • налицо крайне черезжопная защита от переполнения
  • налицо крайне черезжопная методология обработки ошибок: только исключения

Последний пункт прямо указывает, что твое очешуенное решение использовать в продакшене, критичном к производительности нельзя ни в коем случае, т. к. как только пойдет поток битых данных, твоя поделка уведет в полку по CPU все ядра, на которых работает: вместо парсинга ядра будут заняты stack unwinding'ом.

У меня все, можешь уходить в крестоотрицания.

kawaii_neko ★★★★
()
Ответ на: комментарий от andreyu

Это для чисел от 0000 до 9999. Чтоб сделать распознавание от 00000000 до 99999999 надо эту штуку вызвать два раза с разным «смещением» и склеить два числа, которое оно вернет. Склеивается через result = half1 * 10000 + half2.

SZT ★★★★★
()
Ответ на: комментарий от SZT

Если интересно как эти сдвиги работают, могу объяснить на пальцах

Спасибо, вроде и так все очевидно.

andreyu ★★★★★
()

Pure C такую скорость выдать не сможет никогда - в этом сила «плюсов»

В таком виде утверждение ложно, так как на pure C всегда можно написать код, эквивалентный *любому* коду на С++ (естественно, в случае с шаблонами сишный код может оказаться намного больше по объему)

annulen ★★★★★
()

Лох

У меня почти в пять раз быстрее чем strtol, с проверкой OVERFLOW, UNDEFLOW и любой системой счисления с основанием <= 16. Написано минут за 20 тяп-ляп. На итераторы сам переделывай, если хочешь.

#include <cstdlib>
#include <string>
#include <cassert>

enum class ParseError { NO_ERROR, OVERFLOW, UNDERFLOW };

template<class IntT, int BASE = 10>
IntT parse_int(const char* const text, const char** end = nullptr, ParseError* error = nullptr)
{
    const char* p = text;

    if (*p == '\0') {
        if (end) *end = p;
        if (error) *error = ParseError::NO_ERROR;
        return 0;
    }

    IntT result = 0;
    int sign = 1;
    if (*p == '-') {
        sign = -1;
        ++p;
    }
    while (1) {
        const char c = *p;
        int digit = BASE;
        if      (c >= '0' && c <= '9') digit = c - '0';
        else if (c >= 'a' && c <= 'f') digit = 10 + c - 'a';
        else if (c >= 'A' && c <= 'F') digit = 10 + c - 'A';

        if (digit >= BASE) {
            if (end) *end = p;
            if (error) *error = ParseError::NO_ERROR;
            return result;
        }

        const IntT prev_result = result;
        if (sign > 0) {
            result = result*BASE + digit;
            if (result < prev_result) {
                if (end) *end = p;
                if (error) *error = ParseError::OVERFLOW;
                return 0;
            }
        } else {
            result = result*BASE - digit;
            if (result > prev_result) {
                if (end) *end = p;
                if (error) *error = ParseError::UNDERFLOW;
                return 0;
            }
        }

        ++p;
    }
    assert(0);
    return result;
}

static void test()
{
    static const struct {
        const char* text;
        int expected_result;
        char expected_end_char;
        ParseError expected_error;
    } variants[] = {
        { "", 0, '\0', ParseError::NO_ERROR },
        { "0", 0, '\0', ParseError::NO_ERROR },
        { "1", 1, '\0', ParseError::NO_ERROR },
        { "-1", -1, '\0', ParseError::NO_ERROR },
        { "12a", 12, 'a', ParseError::NO_ERROR },
        { "127", 127, '\0', ParseError::NO_ERROR },
        { "-128", -128, '\0', ParseError::NO_ERROR },
        { "128", 0, '8', ParseError::OVERFLOW },
        { "-129", 0, '9', ParseError::UNDERFLOW },
        { "991", 0, '1', ParseError::OVERFLOW },
        { "-991", 0, '1', ParseError::UNDERFLOW }
    };
    for (const auto& v : variants) {
        const char* end = nullptr;
        ParseError error = ParseError::NO_ERROR;
        const int result = parse_int<signed char>(v.text, &end, &error);
        assert(result == v.expected_result);
        assert(*end == v.expected_end_char);
        assert(error == v.expected_error);
    }

    /* binary */
    assert((parse_int<int, 2>("101") == 5));
    /* octal */
    assert((parse_int<int, 8>("21") == 17));
    /* hex */
    assert((parse_int<int, 16>("af1") == 2801));
    assert((parse_int<int, 16>("AF1") == 2801));
}

int main(int argc, const char* argv[])
{
    test();
    const int COUNT = 100000000;
	long result = 0;
	char* end = nullptr;
    if (argc == 1 || (argc > 1 && std::string(argv[1]) == "--strtol")) {
        for (int i = 0; i < COUNT; ++i)
            result += strtol("123", &end, 10);
    } else {
        for (int i = 0; i < COUNT; ++i)
            result += parse_int<long>("123");
    }
    assert(result == 12300000000L);
	return result == 12300000000L ? 0 : 1;
}
g++ --version
g++ (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4
g++ -std=c++11 -Wall -O2 -o parse_int parse_int.cc

time ./parse_int --strtol

real	0m1.034s
user	0m1.033s
sys	0m0.000s

time ./parse_int --parse_int

real	0m0.227s
user	0m0.226s
sys	0m0.003s
anonymous
()
Ответ на: комментарий от dzidzitop

это переносимый способ задать ASCII char code 'a' для non-ASCII среды компиляции.

Why? Где они есть-то? Даже на «попсовой» EBCDIC всё равно буквы не подряд, так что смысла всё равно ноль.

i-rinat ★★★★★
()
Последнее исправление: i-rinat (всего исправлений: 1)
Ответ на: Лох от anonymous

Ровно в 5 раз

Причёсанная и исправленная версия с более точным измерением

#include <cstdlib>
#include <string>
#include <cassert>
#include <limits>

enum class ParseError { NO_ERROR, OVERFLOW, UNDERFLOW };

template<class IntT, int BASE = 10>
IntT parse_int(const char* const text, const char** end = nullptr, ParseError* error = nullptr)
{
    static const unsigned char ch_to_digit[] = {
        16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
        16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
        16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
         0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 16, 16, 16, 16, 16, 16,
        16, 10, 11, 12, 13, 14, 15, 16, 16, 16, 16, 16, 16, 16, 16, 16,
        16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
        16, 10, 11, 12, 13, 14, 15, 16, 16, 16, 16, 16, 16, 16, 16, 16,
        16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
        16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
        16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
        16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
        16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
        16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
        16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
        16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
        16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16
    };

    const char* p = text;
    constexpr IntT min = std::numeric_limits<IntT>::min();
    constexpr IntT max = std::numeric_limits<IntT>::max();

    const auto ret = [&end, &error, &p](IntT res, ParseError err_code) -> IntT
    {
        if (end) *end = p;
        if (error) *error = err_code;
        return res;
    };

    IntT result = 0;
    int sign = 1;
    if (*p == '-') {
        sign = -1;
        ++p;
    }
    while (1) {
        const int digit = ch_to_digit[static_cast<unsigned char>(*p)];
        if (digit >= BASE)
            return ret(result, ParseError::NO_ERROR);

        const IntT prev_result = result;
        if (sign > 0) {
            if (result > max/BASE)
                return ret(0, ParseError::OVERFLOW);
            result = result*BASE + digit;
            if (result < prev_result)
                return ret(0, ParseError::OVERFLOW);
        } else {
            if (result < min/BASE)
                return ret(0, ParseError::UNDERFLOW);
            result = result*BASE - digit;
            if (result > prev_result)
                return ret(0, ParseError::UNDERFLOW);
        }

        ++p;
    }
    assert(0);
    return result;
}
$ g++ -Wall -Wextra -std=c++11 -O2 -o parse_int parse_int.cc
$ time ./parse_int --strtol

real	0m10.159s
user	0m10.150s
sys	0m0.004s
$ time ./parse_int --parse_int

real	0m2.099s
user	0m2.098s
sys	0m0.000s
anonymous
()
Ответ на: комментарий от annulen

если на каждый инстанс шаблона писать версию на C, то да. но в библиотеках это распространять невозможно. т.е. код всегда будет усреднённый = субоптимальный.

dzidzitop ★★
() автор топика
Ответ на: Лох от anonymous

if (result < prev_result) {

вот эта проверка вроде бы ложная.

dzidzitop ★★
() автор топика
Ответ на: Ровно в 5 раз от anonymous

и этот код не работает во всех случаях переполнения. а насчёт таблицы - нужно смотреть что будет с кэшами. проверю и у себя.

dzidzitop ★★
() автор топика
Ответ на: комментарий от dzidzitop

Проверку можно отвязать от тонкостей поведения встроенных типов, но будет где-то на 20% медленней (больше арифметических операций). Если не напрягаться особо. Это экспромт-говнокод же.

Но вообще-то если кого и правда волнует скорость, так делать конечно же будет, а напишет предельно специализированный алгоритм с учётом специфики задачи, который уделает strtol раз в 10 - 20.

anonymous
()
Ответ на: комментарий от dzidzitop

Ну-ка, расскажи мне, как в твоей функции предполагается обрабатывать ошибочный ввод? Функторы на роль ParseError что ли заряжать?

kawaii_neko ★★★★
()
Ответ на: комментарий от kawaii_neko

вызывается errorHandler() - это функтор, который может захватывать переменные контекста и выставлять флаг, например, который будет проверяться после выхода из функции parseNumber(). При этом скорее всего это будет почти бесплатная проверка (переменную на стеке компилятор почти точно создаст).

dzidzitop ★★
() автор топика
Ответ на: комментарий от anonymous

Проверку можно отвязать от тонкостей поведения встроенных типов, но будет где-то на 20% медленней (больше арифметических операций). Если не напрягаться особо. Это экспромт-говнокод же.

можно с учётом type_traits информации сделать if-else, тогда для встроенных типов код будет один, а для остальных - более «безопасный». специализированный алгоритм для выгребания чисел из текста (мне это нужно для парсинга json) - это как раз что-то подобное. в более узких задачах - да, лучше что-то более умное придумать, как например код от SZT

dzidzitop ★★
() автор топика
Ответ на: Ровно в 5 раз от anonymous

небольшая проблема в этом коде - char - это не всегда 8 бит. поэтому для некоторых архитектур этот код может приводить к undefined behaviour. можно добавить бесплатные проверки на «< 256»

dzidzitop ★★
() автор топика
Ответ на: Ровно в 5 раз от anonymous

с таблицей поиска в данном случае код работает чуть быстрее (для неоднозначных чисел). спасибо.

dzidzitop ★★
() автор топика
Ответ на: комментарий от dzidzitop

небольшая проблема в этом коде - char - это не всегда 8 бит. поэтому для некоторых архитектур этот код может приводить к undefined behaviour.

Скорее всего, с точки зрения переносимости не единственная и скорее всего наименьшая проблема. Но лень сверяться со стандартом.

Цель написать оптимальный код, переносимый код или даже просто хороший код и не ставилась. Так, любопытно просто, насколько можно уделать strtol с минимальными умственными усилиями.

Про бенч Царь дело говорит, кстати.

anonymous
()
Ответ на: комментарий от anonymous

бенч должен генерировать строки на лету. твой тест, кстати, (ты же автор кода выше) с константами может очень помогать твоей реализации. не камень в огород, просто нужно смотреть ассемблерный листинг + branch predictor может натренироваться на какой-то паттерн. ну а промахи кеша в бенчмарках самодельных проверять достаточно накладно - а живую нагрузку подбирать геморно.

я как раз смотрел в стандарт насчёт overflow/underflow. вроде как проблем нет с ним для всех возможных процессорных архитектур, которые поддерживаются стандартом. но, как всегда с С++, могу ошибаться. в твоём коде (вторая версия) тоже вроде проблем с этим нет, но не всматривался в ветку где sign == -1

dzidzitop ★★
() автор топика
Ответ на: комментарий от anonymous
#include <afc/number.h>
#include <string>
#include <vector>
#include <cmath>
#include <cstring>

using namespace std;

int main(const int argc, char ** const argv)
{
	vector<string> values;
	for (int i = 0; i < 10000; ++i) {
		values.emplace_back(to_string(rand()));
	}

	unsigned long int strtoul (const char* str, char** endptr, int base);
	const std::size_t n = 10000;
	unsigned long result = 0;
	char *end;
	long p;
	for (int j = 0; j < 10000; ++j)
	for (std::size_t i = 0; i < n; ++i) {
		string &str = values[i];
		//result += strtol(str.c_str(), &end, 16);
		afc::parseNumber<16>(str.begin(), str.end(), p, [](string::iterator){exit(1);});
		result += p;
	}
	return result;
}
dzidzitop ★★
() автор топика
Ответ на: комментарий от dzidzitop

10000 рандомных значений - корреляция результатов высокая. на моём компьютере - код с таблицей - 5.1с - код без таблицы - 4.6с - код gcc - 9.7c

+- разбежка на rand(). запускаю по нескольку раз.

dzidzitop ★★
() автор топика
Ответ на: комментарий от dzidzitop
#include <afc/number.h>
#include <string>
#include <vector>
#include <cmath>
#include <cstring>

using namespace std;

int main(const int argc, char ** const argv)
{
	vector<string> values;
	for (int i = 0; i < 100; ++i) {
		values.emplace_back(to_string(rand() - (RAND_MAX / 2)));
	}

	const std::size_t n = 100;
	unsigned long result = 0;
	char *end;
	long p;
	for (int j = 0; j < 1000000; ++j)
	for (std::size_t i = 0; i < n; ++i) {
		string &str = values[i];
		//result += strtol(str.c_str(), &end, 16);
		afc::parseNumber<16>(str.begin(), str.end(), p, [](string::iterator){exit(1);});
		result += p;
	}
	return result;
}

а эта программа:

- afc::parseNumber() на if - 4.8c

- stroul - 8.5c

dzidzitop ★★
() автор топика
Ответ на: Ровно в 5 раз от anonymous

I see you C++ nightmare and rise you plain C. ;)

#include <assert.h>
#include <stdio.h>

int map[] = {
	['0'] =  0, ['1'] =  1, ['2'] =  2, ['3'] =  3,
	['4'] =  4, ['5'] =  5, ['6'] =  6, ['7'] =  7,
	['8'] =  8, ['9'] =  9, ['a'] = 10, ['A'] = 10,
	['b'] = 11, ['B'] = 11, ['c'] = 12, ['C'] = 12,
	['d'] = 13, ['D'] = 13, ['e'] = 14, ['E'] = 14,
	['f'] = 15, ['F'] = 15, ['g'] = 16, ['G'] = 16,
	['h'] = 17, ['H'] = 17, ['i'] = 18, ['I'] = 18,
	['j'] = 19, ['J'] = 19, ['k'] = 20, ['K'] = 20,
	['l'] = 21, ['L'] = 21, ['m'] = 22, ['M'] = 22,
	['n'] = 23, ['N'] = 23, ['o'] = 24, ['O'] = 24,
	['p'] = 25, ['P'] = 25, ['q'] = 26, ['Q'] = 26,
	['r'] = 27, ['R'] = 27, ['s'] = 28, ['S'] = 28,
	['t'] = 29, ['T'] = 29, ['u'] = 30, ['U'] = 30,
	['v'] = 31, ['V'] = 31, ['w'] = 32, ['W'] = 32,
	['x'] = 33, ['X'] = 33, ['y'] = 34, ['Y'] = 34,
	['z'] = 35, ['Z'] = 35,
};

long
stol(char *s, int base)
{
	long r = 0;
	int  neg;

	if ((neg = *s == '-'))
		s++;

	do {
		assert(map[*s] >= 0 && map[*s] < base);
		r *= base;
		r += map[*s];
	} while (*++s);

	return neg ? -r : r;
}

struct testcase {
	char *string;
	int  base;
	long result;
} *tc, testcases[] = {
	{  "101",  2,    5 },
	{  "122",  3,   17 },
	{  "122",  8,   82 },
	{  "123", 10,  123 },
	{ "-123", 10, -123 },
	{   "FF", 16,  255 },
	{   "ZZ", 36, 1295 },
	{   NULL,  0,    0 },
};

int
main()
{
	for (tc = testcases; tc->string; tc++) {
		printf("%4s (base %2d) expected %4ld, got %4ld (base 10)\n",
			tc->string, tc->base, tc->result, stol(tc->string, tc->base));
	}

	return 0;
}
beastie ★★★★★
()
Ответ на: комментарий от dzidzitop

твой тест, кстати, (ты же автор кода выше)

Ну, тест-то как раз твой ;) быстрый парсинг целочисленных значений (комментарий)

Чуть более адекватный тест уже показывает, что радикальной разницы с strtol на рандомных входных данных просто так не получишь.

anonymous
()
Ответ на: Thank you! от CyberK

/me давясь попкорном: ёш, че нельзя было сделать так ?
У меня, например, гифки не проигрываются автоматом, нужно «плей» нажать в верхнем правом углу. А на сайте в том месте вылазяет какой-то банер.

Но то такое, не отвлекайтесь. Ждем когда Хуан Пабло выйдет из комы. :)

anTaRes ★★★★
()

Чем неудачная? Прокомментируй.

Меня просто немного удивило, что такая, в принципе элементарная, задача раздувается в такие нечитабельные простыни на C++.

PS: с C++ знаком очень мало, мне его даже читать сложно.

beastie ★★★★★
()
Ответ на: комментарий от beastie

Чем неудачная? Прокомментируй.

Начнём с этого:

$ gcc -Wall -Wextra -o plain_c_parse  plain_c_parse.c 
plain_c_parse.c: In function ‘stol’:
plain_c_parse.c:33:3: warning: array subscript has type ‘char’ [-Wchar-subscripts]
   assert(map[*s] >= 0 && map[*s] < base);
   ^
plain_c_parse.c:33:3: warning: array subscript has type ‘char’ [-Wchar-subscripts]
plain_c_parse.c:35:3: warning: array subscript has type ‘char’ [-Wchar-subscripts]
   r += map[*s];
   ^

Как думаешь, что может произойти при компиляции с -fsigned-char?

Дальше: про спецификаторы static и const не слышал? Не понимаешь, зачем они нужны? Особенно const.

элементарная, задача раздувается в такие нечитабельные простыни на C++

В простыни на С++ раздувается задача, совершенно не эквивалентная твоему коду. А эквивалентная задача решается примерно так же компактно.

anonymous
()
Ответ на: комментарий от anonymous

Как думаешь, что может произойти при компиляции с -fsigned-char?

...А что будет, если ему подсунуть «`@$-», независимо он знаковости char'a? То же самое. В этом свете

assert(map[*s] >= 0 && map[*s] < base);

Вообще нелепо.

anonymous
()
Ответ на: комментарий от anonymous

Так хотя бы будет assert. C signed char при определённых входных данных может быть segfault, если «повезёт».

anonymous
()
Ответ на: комментарий от anonymous

Нет — сегфолт при символах, больших «z» — «{», «|», «}», «~», <DEL> ну и верхняя часть при unsigned char — возможен, т.к. выход за пределы.

assert на >= 0 вообще никогда не сработает, т.к. неуказанные индексы устанавливаются в ноль. Нужность второй части тоже сомнительна, при таких дырах.

anonymous
()
Ответ на: комментарий от anonymous

А, блин, точно. У него ж там не [256]. Да, всё ещё круче, чем я думал.

anonymous
()
Ответ на: комментарий от anonymous

assert там вполне лишний (по крайней мере первая часть).

Вторая проблема решается добавлением [0xff] = 0 в массив — это покроет и случаи, где char signed.

beastie ★★★★★
()
Последнее исправление: beastie (всего исправлений: 1)
Ответ на: комментарий от beastie

это покроет и случаи, где char signed

Не покроет, ибо на строке типа «\x81» получишь отрицательный индекс. А дальше как повезёт.

Дырища в безопасности та ещё.

anonymous
()
Ответ на: комментарий от anonymous

Предлагаю такой вот diff:

@@ -17,7 +17,7 @@
 	['t'] = 29, ['T'] = 29, ['u'] = 30, ['U'] = 30,
 	['v'] = 31, ['V'] = 31, ['w'] = 32, ['W'] = 32,
 	['x'] = 33, ['X'] = 33, ['y'] = 34, ['Y'] = 34,
-	['z'] = 35, ['Z'] = 35,
+	['z'] = 35, ['Z'] = 35, [0xff] = 0,
 };

 long
@@ -30,9 +30,10 @@
 		s++;

 	do {
-		assert(map[*s] >= 0 && map[*s] < base);
+		unsigned char i = *s;
+		assert(map[i] < base);
 		r *= base;
-		r += map[*s];
+		r += map[i];
 	} while (*++s);

 	return neg ? -r : r;

beastie ★★★★★
()
Ответ на: комментарий от anonymous

свой тест я смотрел в асм листингах. но он тоже был не самый удачный на самом деле. хотя разбежка в 2-3 раза осталась.

dzidzitop ★★
() автор топика
Ответ на: комментарий от beastie

Меня просто немного удивило, что такая, в принципе элементарная, задача раздувается в такие нечитабельные простыни на C++.

там просто есть поддержка для signed/unsigned integer types одновременно. если вырезать всё, юзать хардкоднутую базу = 10, то всё будет очень просто.

dzidzitop ★★
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.