Arhn - архитектура программирования

Очень быстрая функция приближенного логарифма (натуральный логарифм) в с ++?

Мы находим различные уловки для замены std::sqrt (Timing Square Root), а некоторые для std::exp (Использование более быстрого экспоненциального приближения), но я не нашел ничего, что могло бы заменить std::log.

Это часть циклов в моей программе и вызывается несколько раз, и хотя exp и sqrt были оптимизированы, Intel VTune теперь предлагает мне оптимизировать std::log, после чего кажется, что только мой выбор дизайна будет ограничивать.

Сейчас я использую приближение Тейлора 3-го порядка для ln(1+x) с x между -0.5 и +0.5 (90% случаев для максимальной ошибки 4%) и возвращаюсь к std::log в противном случае. Это дало мне ускорение на 15%.


  • Два голоса "за" через восемь минут за явно не по теме вопрос 02.10.2016
  • Ах, да - вопрос о точности и производительности - но без указания того, какая точность была бы приемлемой или что было опробовано, я не думаю, что вы получите `` ответ '' 02.10.2016
  • Достаточно хорошей точности. Я попытался начать с log2 и преобразовать обратно, но очень быстро log2 просто выводит int, что приводит к очень плохой аппроксимации. Также пытался использовать тот факт, что ln (x) является производной от t- ›x ^ t в t = 0, но это тоже не дает хорошего результата для вычислений. 02.10.2016
  • На современных процессорах std::sqrt компилируется в одну инструкцию. Трудно поверить, что вы можете сделать что-то быстрее, чем это, с такой же точностью. 03.10.2016
  • @plasmacel Пожалуйста, взгляните на ссылку, которую я поместил, вы увидите, что пара инструкций может быть намного быстрее, чем одна. Приятного чтения. 03.10.2016
  • @ user3091460 Если float точности достаточно, почему бы не вызвать logf() из cmath? Или проблема в том, что вам нужен полный входной домен double, но результат вычисляется только с точностью, эквивалентной float (около 6 десятичных цифр)? 03.10.2016
  • @njuffa Я попробую и сравню, что выдает компилятор. 03.10.2016
  • @ user3091460 Что ж, расчет ошибки на этом сайте неверен. sqrtss имеет полную точность, тогда как rsqrtss * x, за которым следует один шаг Ньютона-Рафсона, по-прежнему не дает полной точности. 03.10.2016
  • @ user3091460 Не забудьте также попробовать соответствующие флаги компилятора, в дополнение к -O3 возможно отключение денормальной поддержки (= включение режима FTZ [flush-to-zero]), включение быстрой математики, включение использования векторной математики. библиотека (например, с компилятором Intel). 03.10.2016
  • @ user3091460 Это очень похоже: stackoverflow.com/questions/1528727/ 03.10.2016
  • Почему вы думаете, что ваша реализация std::log еще не использует самый эффективный алгоритм, доступный для вашей системы? Если вы готовы пожертвовать точностью ради скорости (я могу сказать кое-что о быстром получении неправильных ответов), вам нужно указать это в своем вопросе. 03.10.2016
  • На данный момент я использую приближение Тейлора третьего порядка для ln (1 + x) с x между -0,5 и +0,5 (90% случаев для максимальной ошибки 4%) и в противном случае возвращаюсь к std :: log. Дала мне ускорение на 15%. 03.10.2016

Ответы:


1

Прежде чем приступить к разработке и развертыванию индивидуализированной реализации трансцендентной функции повышения производительности, настоятельно рекомендуется провести оптимизацию на алгоритмическом уровне, а также с помощью цепочки инструментов. К сожалению, у нас нет никакой информации ни о коде, который нужно оптимизировать, ни о инструментальной цепочке.

На алгоритмическом уровне проверьте, действительно ли все вызовы трансцендентных функций необходимы. Возможно, существует математическое преобразование, которое требует меньшего количества вызовов функций или преобразует трансцендентные функции в алгебраические операции. Возможно ли, что какие-либо вызовы трансцендентных функций избыточны, например потому что вычисления без надобности включают и выключают логарифмическое пространство? Если требования к точности невысоки, можно ли все вычисления выполнить с одинарной точностью, используя float вместо double? На большинстве аппаратных платформ отказ от double вычислений может привести к значительному увеличению производительности.

Компиляторы, как правило, предлагают множество переключателей, влияющих на производительность кода с большим количеством чисел. Помимо увеличения общего уровня оптимизации до -O3, часто есть способ отключить ненормальную поддержку, то есть включить режим сброса до нуля или FTZ. Это дает преимущества в производительности на различных аппаратных платформах. Кроме того, часто существует быстрый математический флаг, использование которого приводит к небольшому снижению точности и устраняет накладные расходы на обработку особых случаев, таких как NaN и бесконечности, а также обработку errno. Некоторые компиляторы также поддерживают автоматическую векторизацию кода и поставляются с математической библиотекой SIMD, например компилятор Intel.

Специальная реализация функции логарифмирования обычно включает разделение двоичного аргумента с плавающей запятой x на экспоненту e и мантиссу m, так что x = m * 2 e, следовательно log(x) = log(2) * e + log(m). m выбирается так, чтобы он был близок к единице, поскольку это обеспечивает эффективные аппроксимации, например log(m) = log(1+f) = log1p(f) с помощью минимаксного многочлена приближение.

C ++ предоставляет функцию frexp() для разделения операнда с плавающей запятой на мантиссу и экспоненту, но на практике обычно используются более быстрые машинно-зависимые методы, которые манипулируют данными с плавающей запятой на битовом уровне, повторно интерпретируя их как целые числа того же размера. Приведенный ниже код для логарифма с одинарной точностью logf() демонстрирует оба варианта. Функции __int_as_float() и __float_as_int() обеспечивают переинтерпретацию int32_t в число с плавающей запятой IEEE-754 binary32 и наоборот. Этот код в значительной степени полагается на объединенную операцию умножения-сложения FMA, поддерживаемую непосредственно аппаратным обеспечением на большинстве современных процессоров, CPU или GPU. На платформах, где fmaf() отображается на программную эмуляцию, этот код будет неприемлемо медленным.

#include <cmath>
#include <cstdint>
#include <cstring>

float __int_as_float (int32_t a) { float r; memcpy (&r, &a, sizeof r); return r;}
int32_t __float_as_int (float a) { int32_t r; memcpy (&r, &a, sizeof r); return r;}

/* compute natural logarithm, maximum error 0.85089 ulps */
float my_logf (float a)
{
    float i, m, r, s, t;
    int e;

#if PORTABLE
    m = frexpf (a, &e);
    if (m < 0.666666667f) {
        m = m + m;
        e = e - 1;
    }
    i = (float)e;
#else // PORTABLE
    i = 0.0f;
    if (a < 1.175494351e-38f){ // 0x1.0p-126
        a = a * 8388608.0f; // 0x1.0p+23
        i = -23.0f;
    }
    e = (__float_as_int (a) - __float_as_int (0.666666667f)) & 0xff800000;
    m = __int_as_float (__float_as_int (a) - e);
    i = fmaf ((float)e, 1.19209290e-7f, i); // 0x1.0p-23
#endif // PORTABLE
    /* m in [2/3, 4/3] */
    m = m - 1.0f;
    s = m * m;
    /* Compute log1p(m) for m in [-1/3, 1/3] */
    r =             -0.130310059f;  // -0x1.0ae000p-3
    t =              0.140869141f;  //  0x1.208000p-3
    r = fmaf (r, s, -0.121483512f); // -0x1.f198b2p-4
    t = fmaf (t, s,  0.139814854f); //  0x1.1e5740p-3
    r = fmaf (r, s, -0.166846126f); // -0x1.55b36cp-3
    t = fmaf (t, s,  0.200120345f); //  0x1.99d8b2p-3
    r = fmaf (r, s, -0.249996200f); // -0x1.fffe02p-3
    r = fmaf (t, m, r);
    r = fmaf (r, m,  0.333331972f); //  0x1.5554fap-2
    r = fmaf (r, m, -0.500000000f); // -0x1.000000p-1  
    r = fmaf (r, s, m);
    r = fmaf (i,  0.693147182f, r); //  0x1.62e430p-1 // log(2)
    if (!((a > 0.0f) && (a < INFINITY))) {
        r = a + a;  // silence NaNs if necessary
        if (a  < 0.0f) r = INFINITY - INFINITY; //  NaN
        if (a == 0.0f) r = -INFINITY;
    }
    return r;
}

Как отмечено в комментарии к коду, приведенная выше реализация обеспечивает точно округленные результаты с одинарной точностью и имеет дело с исключительными случаями, соответствующими стандарту с плавающей запятой IEEE-754. Производительность можно еще больше повысить, исключив поддержку особых случаев, исключив поддержку денормальных аргументов и снизив точность. Это приводит к следующему примерному варианту:

/* natural log on [0x1.f7a5ecp-127, 0x1.fffffep127]. Maximum relative error 9.4529e-5 */
float my_faster_logf (float a)
{
    float m, r, s, t, i, f;
    int32_t e;

    e = (__float_as_int (a) - 0x3f2aaaab) & 0xff800000;
    m = __int_as_float (__float_as_int (a) - e);
    i = (float)e * 1.19209290e-7f; // 0x1.0p-23
    /* m in [2/3, 4/3] */
    f = m - 1.0f;
    s = f * f;
    /* Compute log1p(f) for f in [-1/3, 1/3] */
    r = fmaf (0.230836749f, f, -0.279208571f); // 0x1.d8c0f0p-3, -0x1.1de8dap-2
    t = fmaf (0.331826031f, f, -0.498910338f); // 0x1.53ca34p-2, -0x1.fee25ap-2
    r = fmaf (r, s, t);
    r = fmaf (r, s, f);
    r = fmaf (i, 0.693147182f, r); // 0x1.62e430p-1 // log(2) 
    return r;
}
02.10.2016
  • Спасибо за это, однако я не могу найти int_as_float и float_as_int, используя Msvc 15 на win10. Я обнаружил, что это часть cuda, но не загрузил полный пакет. 03.10.2016
  • @ user3091460 Эти функции являются абстракциями машинно-зависимых функций. В качестве первого шага вы можете просто использовать memcpy(), например float __int_as_float(int32_t a) { float r; memcpy (&r, &a, sizeof(r)); return r;} Хороший компилятор, скорее всего, оптимизирует это соответствующим образом, но в зависимости от оборудования, на которое вы нацеливаетесь (которое вы не раскрыли), могут быть лучшие способы, возможно, с использованием встроенных функций или встроенной сборки. 03.10.2016
  • @ user3091460 и njuffa: оптимальный asm для x86, вероятно, будет выполнять любые манипуляции с числами с плавающей запятой как целыми числами с использованием целочисленных инструкций SSE2, потому что регистры XMM используются как для скалярных / векторных чисел с плавающей запятой, так и для векторных целых чисел. Так что вам, вероятно, следует _mm_set_ss(your_float) и _mm_castps_si128 это, чтобы получить __m128i, которым вы можете манипулировать. (Это может привести к потере инструкции, обнуляющей старшие биты регистра xmm, из-за конструктивных ограничений встроенных функций.). Также может подойти MOVD для передачи битов с плавающей запятой в / из целочисленного регистра. 03.10.2016
  • @PeterCordes Понятно. Я не собирался вкладывать значительные средства в создание встроенного решения SIMD под ключ, особенно учитывая, что до сих пор неясно, какие расширения ISA доступны на оборудовании запрашивающего. Подумайте о публикации своей собственной версии с использованием встроенных функций SIMD, и я буду рад проголосовать за нее. 03.10.2016
  • Я просто перейду к эффективному float_to_int, который использует объединение для ввода каламбура и компилируется в один movd eax, xmm0 с помощью clang и gcc для x86. godbolt.org/g/UCePpA. Это так же просто, как и следовало ожидать, @ user3091460 :) Манипулирование целым числом как uint32_t может быть даже более эффективным, поскольку целочисленные инструкции короче, а на Haswell могут работать на порту 6 (у которого нет вектора ALU). Но, вероятно, было бы лучше остаться в регистрах XMM, так как вы не очень много работаете с целыми числами. 03.10.2016
  • @PeterCordes Обратите внимание, что версия, основанная на memcpy (), также отлично работает и избегает неопределенного поведения (прокалывание типов AFAIK через союзы не санкционировано стандартом C ++): godbolt.org/g/VXo7gj 03.10.2016
  • @njuffa: Да ладно, это вопрос C ++, не C. Компиляторы довольно хорошо оптимизируют memcpy, но я подозреваю, что в более сложных случаях вы все равно можете получить фактическое сохранение / перезагрузку. GNU C ++ гарантирует, что каламбур типов на основе объединения работает так же, как и в C. Я думаю, что большинство других компиляторов C ++ гарантируют то же самое, но я думаю, что если вы получите хорошие результаты от полностью переносимого способа на целевой цели, о которой вы заботитесь, тогда переходите к тот. 03.10.2016
  • Как вы создали для них полиномы? 04.01.2019
  • @SeanMcAllister Я генерирую минимаксное приближение, используя алгоритм Ремеза, часто с последующим уточнением на основе эвристического поиска. 04.01.2019
  • Что вы используете для создания приближения? Солля? Mathematica была не очень хороша в этом. Можете ли вы объяснить свой трюк с попаданием поплавка в диапазон [-1 / 3, + 1/3]? 05.01.2019
  • @SeanMcAllister Я использую свой софт на основе Remez. Инструмент Sollya должен работать хорошо (сам не использовал, но работал с коллегой, который использовал). 0x3f2aaaab - это двоичное представление 2/3 в стандарте IEEE-754 с одинарной точностью (я, вероятно, должен был добавить туда комментарий), поэтому вы можете изменить это, чтобы уменьшить до других диапазонов, настроив константу. Остальное - это простая манипуляция с битами для разделения экспоненты и мантиссы (0xff800000 охватывает поля знака и показателя). 05.01.2019
  • @SeanMcAllister Итак, 0x3f2aaaab используется для уменьшения до [2/3, 4/3]; это нижняя граница интервала. Другие коды, с которыми вы можете столкнуться, будут сокращены до [sqrt (2) / 2, sqrt (2)], и в этом случае можно будет использовать 0x3f3504f3. Я также экспериментировал с уменьшением до [3/4, 3/2], и в этом случае константа равна 0x3f400000 (константы с небольшим количеством битов более эффективны на некоторых архитектурах). 05.01.2019
  • @njuffa В чем может быть причина, по которой я получаю my_logf () * 0.4342944819f в 2 раза быстрее, чем std :: log10 (), но std :: log10 () по сравнению с x42 быстрее, чем my_faster_logf () * 0.4342944819f (Ubuntu / последний используемый GCC )? Мой процессор - старый i5-750. Кроме того, если бы я ввел только диапазон [0,1], чтобы сделать преобразование линейным в дБ, могли бы ваши функции быть еще быстрее ... но какие все изменения необходимо сделать для достижения достаточной точности преобразования 0,001 дБ? 26.03.2021
  • @JuhaP Извините, я не понимаю, о чем вы спрашиваете. Это сайт вопросов и ответов, а не дискуссионный форум. Подумайте о том, чтобы задать вопрос, соответствующий сайту, если поиск потенциальных дубликатов на этой стороне не дал результатов. 26.03.2021
  • Спасибо за быстрый ответ. Хорошо, забудьте последний вопрос, мой главный вопрос заключался в том, почему эти две ваши функции logf () имеют такую ​​огромную разницу в производительности (в вашем тексте упрощенная функция my_faster_logf () должна повысить производительность, но для меня она в сто раз медленнее, чем ваш my_logf () функция есть)? 26.03.2021
  • @JuhaP Если вы компилируете с одним и тем же компилятором для той же платформы, используя одни и те же переключатели компиляции, скорость my_faster_logf() должна быть примерно вдвое выше, чем my_logf(). Если ваша платформа не поддерживает аппаратно операции слияния умножения и сложения (FMA), вызовы fmaf() будут очень медленными, поскольку необходимо будет эмулировать функциональность. 26.03.2021
  • @njuffa Я тоже так думал, но на практике все не так. Я использую одни и те же переключатели, даже сравнивая друг с другом (тест, я использую значение результатов 421.346 для my_faster_logf () против 4.17925 для my_logf () (std :: log10 () дает результат ~ 9.7), поэтому my_faster_logf () делает не работают должным образом (по крайней мере, в случае тестирования производительности). Вот ссылка на используемые мной тестовые процедуры: kvraudio.com/forum/viewtopic.php?p=7170633#p7170633 26.03.2021
  • @JuhaP Core i5-750 (Lynnfield, приблизительно 2010 г.) не имеет аппаратного обеспечения FMA. Поэтому я ожидал, что fmaf() будет очень медленным на этом процессоре. Операция FMA была добавлена ​​в Haswell в 2013 году. Извините, я не собираюсь отлаживать за вас. Возможно, вам потребуется посмотреть сгенерированный код. Я только что еще раз подтвердил (другой компилятор и другой процессор, чем когда я первоначально опубликовал этот код), что my_faster_logf() работает примерно в два раза быстрее, чем my_logf() [PORTABLE = 0]. 26.03.2021
  • Хорошо спасибо. Попробую отладить сгенерированный код. 26.03.2021
  • @JuhaP Для платформ x86-64 я вижу около 20 инструкций, сгенерированных для my_faster_logf(), по сравнению с примерно 45 инструкциями для быстрого пути для my_logf(). 26.03.2021
  • @njuffa Хорошо, теперь, когда вы убрали все функции my_logf () из if ((a ›0.0f) && (a‹ = 3.40282347e + 38f)) {// 0x1.fffffep + 127} else {// заглушить NaN }, это приближение поддерживает эмулируемый режим. Теперь я получаю 1023,46 для приближения my_logf () ... что я считаю нормальным результатом по сравнению с 421,346 для my_faster_logf () ... и это также доказывает, что здесь эмулируется fmaf (). 27.03.2021
  • @JuhaP Сегодняшнее обновление кода просто переместило обработку особых случаев в конец, и сгенерированный ассемблерный код минимально отличается от сгенерированного для предыдущей версии кода. На моей машине (Intel Xeon W 2133; Skylake-W) я вижу приблизительную пропускную способность: один каждые 2,3 нс для my_faster_logf() и один каждые 5,8 нс для my_logf(), а также встроенный компилятор MSVC logf() каждые 4,8 нс. Для быстрого logf на [0,1) я бы предложил безоговорочно умножить ввод my_faster_logf() на 16777216 (= 2 ** 24) и вычесть 16.63553233f из его результата. 27.03.2021

  • 2

    Взгляните на это обсуждение, принятый ответ относится к реализация функции для вычисления логарифмов на основе разложения Цекендорфа.

    В комментариях к файлу реализации обсуждается сложность и некоторые уловки для достижения O (1).

    Надеюсь это поможет!

    02.10.2016
  • Я посмотрю, спасибо 03.10.2016

  • 3

    Я векторизовал ответ @njuffa. натуральный журнал, работает с AVX2:

    inline __m256 mm256_fmaf(__m256 a, __m256 b, __m256 c){
        return _mm256_add_ps(_mm256_mul_ps(a, b), c);
    }
    
    //https://stackoverflow.com/a/39822314/9007125
    //https://stackoverflow.com/a/65537754/9007125
    // vectorized version of the answer by njuffa
    /* natural log on [0x1.f7a5ecp-127, 0x1.fffffep127]. Maximum relative error 9.4529e-5 */
    inline __m256 fast_log_sse(__m256 a){
    
        __m256i aInt = *(__m256i*)(&a);
        __m256i e =    _mm256_sub_epi32( aInt,  _mm256_set1_epi32(0x3f2aaaab));
                e =    _mm256_and_si256( e,  _mm256_set1_epi32(0xff800000) );
            
        __m256i subtr =  _mm256_sub_epi32(aInt, e);
        __m256 m =  *(__m256*)&subtr;
    
        __m256 i =  _mm256_mul_ps( _mm256_cvtepi32_ps(e), _mm256_set1_ps(1.19209290e-7f));// 0x1.0p-23
        /* m in [2/3, 4/3] */
        __m256 f =  _mm256_sub_ps( m,  _mm256_set1_ps(1.0f) );
        __m256 s =  _mm256_mul_ps(f, f); 
        /* Compute log1p(f) for f in [-1/3, 1/3] */
        __m256 r =  mm256_fmaf( _mm256_set1_ps(0.230836749f),  f,  _mm256_set1_ps(-0.279208571f) );// 0x1.d8c0f0p-3, -0x1.1de8dap-2
        __m256 t =  mm256_fmaf( _mm256_set1_ps(0.331826031f),  f,  _mm256_set1_ps(-0.498910338f) );// 0x1.53ca34p-2, -0x1.fee25ap-2
    
               r =  mm256_fmaf(r, s, t);
               r =  mm256_fmaf(r, s, f);
               r =  mm256_fmaf(i, _mm256_set1_ps(0.693147182f),  r);  // 0x1.62e430p-1 // log(2)
        return r;
    }
    
    02.01.2021
  • Обратите внимание, что ваш mm256_fmaf может компилироваться как отдельные операции mul и add с округлением промежуточного продукта. Это не гарантированно является FMA. (И только некоторые компиляторы, такие как GCC, заключат его в инструкцию FMA для вас, когда цель поддерживает FMA, как это делает большинство машин AVX2 (не совсем все: одна конструкция VIA). Вероятно, лучше всего просто нацелить AVX2 + FMA3 и использовать _mm256_fmadd_ps , может быть, с необязательной альтернативой, если хотите, но не с ошибочно названной и, возможно, более медленной fma функцией по умолчанию. 03.01.2021

  • 4

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

    02.10.2016
  • Я ищу натуральный логарифм для реального математического приложения, не нужна двойная точность, точность с плавающей запятой или даже 10-3, 10-4 было бы хорошо 02.10.2016
  • ссылка на книгу без цитирования соответствующих частей не является ответом 02.10.2016

  • 5
  • Ваш каламбур нарушает строгий псевдоним. (Используйте memcpy вместо приведения указателя. Также вам, вероятно, следует использовать unsigned long long, потому что вам не нужен арифметический сдвиг. Не имеет значения для правильности на машине с дополнением 2, но все же.) Это также требует, чтобы целочисленный порядок байтов соответствовал порядку байтов с плавающей запятой. , как на x86, так что вы должны хотя бы задокументировать это. 04.09.2018
  • Некоторый текст, объясняющий стратегию поиска в таблице и фактическую относительную / абсолютную точность для некоторого диапазона входных данных, а также ограничения, такие как то, что происходит для 0 или отрицательных входных значений, было бы хорошей идеей. 04.09.2018
  • Ваша таблица должна быть только float. Это сократит объем вашего кеш-памяти вдвое. (Но таблица размером 2 ^ 14 * 4 байта по-прежнему имеет размер 64 КБ. В большинстве случаев вы получите много промахов в кеше, поэтому большинство реализаций быстрого журнала используют полиномиальное приближение на современных процессорах, а не поиск в таблице. Особенно когда можно использовать SIMD: Эффективная реализация log2 (__ m256d) в AVX2) 04.09.2018
  • Питер, извините за комментарий к очень старому ответу, но действительно ли здесь нарушается правило строгого псевдонима? Я предполагаю, что вы имеете в виду самую первую строку в функции fast_log2. Я бы предположил, что здесь действительно нет псевдонима и что значение x копируется, переинтерпретируется как long long (так что поведение очень похоже на memcpy). Если я чего-то не упускаю, это не псевдоним, верно? 27.05.2021
  • Новые материалы

    Коллекции публикаций по глубокому обучению
    Последние пару месяцев я создавал коллекции последних академических публикаций по различным подполям глубокого обучения в моем блоге https://amundtveit.com - эта публикация дает обзор 25..

    Представляем: Pepita
    Фреймворк JavaScript с открытым исходным кодом Я знаю, что недостатка в фреймворках JavaScript нет. Но я просто не мог остановиться. Я хотел написать что-то сам, со своими собственными..

    Советы по коду Laravel #2
    1-) Найти // You can specify the columns you need // in when you use the find method on a model User::find(‘id’, [‘email’,’name’]); // You can increment or decrement // a field in..

    Работа с временными рядами спутниковых изображений, часть 3 (аналитика данных)
    Анализ временных рядов спутниковых изображений для данных наблюдений за большой Землей (arXiv) Автор: Рольф Симоэс , Жильберто Камара , Жильберто Кейрос , Фелипе Соуза , Педро Р. Андраде ,..

    3 способа решить квадратное уравнение (3-й мой любимый) -
    1. Методом факторизации — 2. Используя квадратичную формулу — 3. Заполнив квадрат — Давайте поймем это, решив это простое уравнение: Мы пытаемся сделать LHS,..

    Создание VR-миров с A-Frame
    Виртуальная реальность (и дополненная реальность) стали главными модными терминами в образовательных технологиях. С недорогими VR-гарнитурами, такими как Google Cardboard , и использованием..

    Демистификация рекурсии
    КОДЕКС Демистификация рекурсии Упрощенная концепция ошеломляющей О чем весь этот шум? Рекурсия, кажется, единственная тема, от которой у каждого начинающего студента-информатика..