Как я реализовывал алгоритм маскирования для протокола WebSocket на Wolfram Language и подключал Си-библиотеку

от автора

Арнольд замаскировался под местность, но кто-то в этих джунглях маскируется еще лучше

Арнольд замаскировался под местность, но кто-то в этих джунглях маскируется еще лучше

Всем привет! Эта статья — про очень простой алгоритм маскирования данных при пересылке по протоколу WebSocket. Но рассказать я хочу не про сам алгоритм, а про путь оптимизации, который я прошел, чтобы сделать его эффективным. Я уверен, что можно еще лучше и если так, то надеюсь уважаемые читатели мне подскажут! Приступим…

WebSocket

Это протокол общения, который работает поверх HTTP. В момент создания соединения клиент отправляет серверу «рукопожатие» примерно вот такого вида:

GET / HTTP/1.1 Connection: Upgrade Upgrade: websocket Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== ...

Сервер отвечает примерно вот так:

HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= ...

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

Framing

После того как соединение установлено клиент и сервер отправляют друг другу так называемые «фреймы». Это сообщения со специально структурой. Подробно о них можно прочитать в спецификации. Но если коротко, то они состоят из:

[metadata][length][masking-key][payload-data]
  • metadata — некая информация о сообщении и резервные биты

  • length — длина сообщения

  • masking-key — ключ для размаскирования

  • payload-data — сами данные

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

Нас интересует сам способ маскирования!

Masking/unmasking

Есть ключ — это 4 случайных байта. Нужно взять его и пройтись по всему массиву данных операцией побитового исключения. Допустим вот ключ:

key = {0, 1, 2, 3}; 

Вот данные:

payload = {0, 1, 2, 3, 4, 5}

Вот что делает функция маскирования (^ — побитовое исключение):

k ^ p == m 0 ^ 0 == 0 1 ^ 1 == 0 2 ^ 2 == 0 3 ^ 3 == 0 0 ^ 4 == 4 1 ^ 5 == 4

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

Проблема

А в чем проблема-то? Дело в том, что мы (мы — это группа из двух разработчиков WLJS Notebook) в своем проекте используем язык Wolfram. А он, как известно тем, кто этот язык не использует, «очень медленный», так как это символьный и интерпретируемый язык. И когда мы реализовывали протокол WebSocket для Wolfram Language (WL), то в полной мере это почувствовали. Дело в том, что нам требуется максимально быстрая скорость ответа сервера по протоколу WebSocket, но самая первая наивная реализация маскирования была медленная и тормозила взаимодействие браузера с сервером. Поэтому я занялся поиском путей решения этой проблемы. Этот путь я и изложу ниже.

Стандартная реализация

Итак, у нас язык Wolfram и алгоритм маскирования на нем выглядит вот так:

mask1 = Function[{maskingKey, payload},    ByteArray[Table[     BitXor[payload[[i]], maskingKey[[Mod[i - 1, 4] + 1]]],      {i, 1, Length[payload]}   ]] ]; 
  • Function[{args}, expr] — чистая функция

  • ByteArray[] — мы должны вернуть массив байт для записи в сокет

  • Table[f[i], {i, 1, n}] — создает список с помощью цикла

  • BitXor[a, b] — побитовое исключение

  • Mod[a, b] — остаток от деления

Создадим один ключ:

maskingKey = ByteArray[{192, 168, 13, 7}]

И набор данных:

payload = StringToByteArray["{\"type\": \"point\", \"x\": 1.23, \"y\": 4.56}"]

Применим функцию маскирования:

payloadMasked = mask1[maskingKey, payload] (* => ByteArray[..] *)

В результате мы передали на вход два массива байт и получили на выходе один массив. Отобразить его в виде строки не получится, так как он содержит символы, которые не используются в UTF-8. Но мы знаем, что это нечитаемый текст. Еще у маскирования есть одно важное свойство! Повторное применение восстанавливает данные, т.е. выполняет размаскирование. Это значит что клиент присылает данные, сервер их размаскирует, затем создает ответ и маскирует той же функцией и отправляет клиенту. Клиент в свою очередь восстанавливает их той же операцией, зная ключ из заголовка фрейма. Получается, что выполняется следующее равенство:

mask1[maskingKey, mask1[maskingKey, payload]] == payload
Весь код выше с результатами

Весь код выше с результатами

Быстродействие

Замеры проводятся на ноутбуке ASUS TUF с Windows 11 и процессором AMD Ryzen 4800H 2.90 GHz. Но нас конечно в первую очередь будет интересовать относительный прирост производительности.

Характеристики системы для чистоты эксперимента

Характеристики системы для чистоты эксперимента

А теперь проверим насколько быстра и эффективна первая наивная реализация. Запустим код несколько раз на данных длиной 1 Мб:

payload1Mb = ByteArray[RandomInteger[{0, 255}, 2^20]]

И замер времени:

t1 = RepeatedTiming[mask1[maskingKey, payload1Mb]]
Первый результат для 1 мегабайта

Первый результат для 1 мегабайта

Запомним этот результат. От него мы и будем отталкиваться в дальнейших замерах. Уважаемым читателям, может показаться, что 1.7 секунд на 1 мегабайт информации это какое-то издевательство. Все так и есть. И этому эффекту есть объяснение, если очень глубоко покопаться в устройстве языка. Я постараюсь ответить на вопрос какого почему таааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааак долго.

Джон недоумевает

Джон недоумевает

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

  • WL — это язык с неизменяемыми структурами данных.
    Это значит, что практически любое выражение, переменная, массив и все что угодно созданное в языке является константами. Всякий раз когда мы пытаемся, например, изменить значение в списке чисел — происходит создание нового списка. Т.е. это жутко неэффективно, но зато надежно и безопасно если писать код в функциональном стиле.

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

  • Он интерпретируемый.
    Это главная проблема. Ведь каждая атомарная операция проходит долгий путь прежде чем выполниться на процессоре. Вот как этот путь выглядит:

[user] -> [ui] -> [interpreter] -> [byte-code] -> [machine code] -> [cpu]

т.е. сначала пользователь вводит строку, на UI она выглядит как аналог HTML-разметки в виде так называемых «боксов», UI разбирает боксы и вытаскивает из них выражение, выражение интерпретатором превращается в полную форму, полная форма компилируется в байт-код виртуальной машины ядра, байт-код отправляется на ядро, ядро исполняет байт код превращая его в машинный код и отправляя на процессор. Затем эта цепочка выполняется в обратную сторону и заканчивается все тем, что фотоны/электромагнитные волны испущенные экраном компьютера попадают на сетчатку глаза и нервные импульсы двигаются в мозг передавая ответ ядра пользователю. То есть вся эта длинная цепочка выполняется на каждое атомарное действие. В нашем коде первый этап правда выполнится один раз, но затем каждый шаг цикла будет проходить через байт код и машинный код, а затем обратно. Сами по себе операции на CPU выполняются быстро, но если их очень много то эффективность сильно падает.

Еще одним важным фактором является то, что мы записали в наивной версии все очень просто и мы не обращали внимание на типы данных. На них тоже нужно остановиться. Как мы понимаем из условия на вход передаются массивы байт. Массив байт — это сложный композитный объект. На абсолютно каждой итерации цикла это объект целиком прогоняется до процессора и обратно, где у массива извлекается i-ый элемент. Что конечно же абсолютно неэффективно. Дело в том, что из-за сложной структуры массива байт, язык не способен применить одну из своих технологий, которая называется «автокомпиляция». Она срабатывает для численных алгоритмов, в тех случаях когда это простой цикл с численными операциями. Тогда ядро понимает, что ему не нужно каждый переход от итерации к итерации гонять через виртуальную машину и обратно. А выполняет итерации по циклу сразу в виртуальной машине, что сокращает оверхед. Но как этого добиться?

Автокомпиляция

Чтобы заставить ядро использовать эту технологию мы должны избавиться от того, что мешает этот код ускорить. У нас должны быть на входе два списка чисел и на выходе список чисел. Значит массив байт надо превратить в список перед циклом вот так:

mask2 = Function[{maskingKey, payload},  Module[{k = Normal[maskingKey], p = Normal[payload]},  ByteArray[Table[ BitXor[p[[i]], k[[Mod[i - 1, 4] + 1]]],  {i, 1, Length[p]} ]] ]];   t2 = RepeatedTiming[mask2[maskingKey, payload1Mb]]
Здесь применилась автокомпиляция

Здесь применилась автокомпиляция

В примере выше внутри цикла уже не осталось сложного объекта ByteArray, который мешал применить автокомпиляцию. Именно этим объясняется такое ускорение сразу в 23 раза. Важно помнить этой свойство языка и правильно работать с типами.

Compile

Если WL умеет самостоятельно применять автокомпиляцию — т.е. отбрасывать промежуточные шаги при обращении к ядру, то может быть можно и в ручную заставить язык это сделать? Конечно, да! Именно для этого предназначена функция Compile. Она создает байт-код для ядра, который не выполняется построчно, а весь исполняется в виртуальной машине. Вот как можно переделать код выше:

cmask3 = Compile[{{k, _Integer, 1}, {p, _Integer, 1}},  Table[ BitXor[p[[i]], k[[Mod[i - 1, 4] + 1]]],  {i, 1, Length[p]} ] ];   mask3[maskingKey_ByteArray, payload_ByteArray] :=  Module[{k = Normal[maskingKey], p = Normal[payload]},  ByteArray[cmask3[k, p]] ]  t3 = RepeatedTiming[mask3[maskingKey, payload1Mb]]
Ускорение еще на ~20%

Ускорение еще на ~20%

И снова внимательно посмотрим на типы данных. На вход передаются массивы байт. Специальная структура данных для хранения чисел с типом «UnsignedInteger8». Она хранит их наиболее эффективно. Но когда мы вызываем функцию Normal, то массив байт превращается в список целых чисел с типом long ли Integer64. То есть размер массива увеличивается в 8 раз, а эффективность атомарных операцией на 64-битных числах падает. Поему мы используем long? Увы, таковы ограничения Compile. Эта функция может работать только с 64-битными целыми, действительными машинной точности, комплексными числами и булевыми значениями True|False. Кроме того Compile работает с тензорами из перечисленных типов. Т.е. передать туда массив из чисел 8-битной точности невозможно. Есть ли способ это сделать другим путем? Конечно, да!

FunctionCompile

Начиная с 12 версии в языке появилась функция FunctionCompile. Это функция делает примерно тоже самое, что и Compile, т.е. превращает код на языке Wolfram в байт-код. Но если Compile возвращает функцию для вызывает байт-кода на виртуальной машине WL, то FunctionCompile компилирует IR-код для LLVM. Это самое важное отличие в концептуальном плане нового способа компиляции от старого. Еще одним отличием является то, что FunctionCompile поддерживает гораздо больше типов данных. И одним из них является как раз нужный нам массив байт. Что ж, возьмем тот же алгоритм и перепишем его вот так:

cmask4 = FunctionCompile[Function[ { Typed[maskingKey, "NumericArray"::["MachineInteger", 1]],  Typed[payload, "NumericArray"::["MachineInteger", 1]] },  Table[ BitXor[payload[[i]], maskingKey[[Mod[i - 1, 4] + 1]]],  {i, 1, Length[payload]}     ] ]];   mask4[maskingKey_ByteArray, payload1Mb_ByteArray] :=  ByteArray[cmask4[maskingKey, payload1Mb]]  t4 = RepeatedTiming[mask4[maskingKey, payload1Mb]]

Синтаксис очень похож на Compile, только теперь нужно использовать двойную конструкцию из FunctionCompile[Function[]], а типы аргументов следует указывать явно. Важно, что такая скомпилированная функция сразу может принимать на вход массивы байт без дополнительных преобразований. Но возвращает по прежнему список, который мы приводим к типу ByteArray[].

Компиляция для LLVM здесь явно выигрывает!

Компиляция для LLVM здесь явно выигрывает!

Опять же, внимательный читатель заметит, что я снова использовал 64-битные целые, вместо беззнаковых 8-битных. Все потому, что FunctionCompile в таком виде, как записано выше по умолчанию выводит тип, который возвращает Table как «PackedArray»::[«MachineInteger», 1]. Т.е. это упакованный обычный список. Можно ли заставить Table явно возвращать массив байт? Увы, ответ — нет. Внутри скомпилированного кода функция Table может вернуть только упакованный список. Но можно переписать код с использованием другого цикла, как это принято делать в некоторых других языках. Сначала мы создаем копию входного массива, а затем в цикле изменяем его значения выполняя побитовое исключение. Почему не менять сам входной массив? Потому что входные параметры неизменяемы и это сделать невозможно внутри FunctionCompile. Вот как выглядит новый код:

dec5 = FunctionDeclaration[ fmask5,  Typed[{ "NumericArray"::["UnsignedInteger8", 1],  "NumericArray"::["UnsignedInteger8", 1] } -> "NumericArray"::["UnsignedInteger8", 1]] @  Function[{maskingKey, payload},  Module[{result = payload},  Do[ result[[i]] =  BitXor[payload[[i]], maskingKey[[Mod[i - 1, 4] + 1]]],  {i, 1, Length[payload]} ];  result ] ] ];   cmask5 = FunctionCompile[dec5, fmask5];   mask5[maskingKey_ByteArray, payload1Mb_ByteArray] :=  ByteArray[cmask5[maskingKey, payload1Mb]]
  • FunctionDeclaration[name, dec] — декларирует сигнатуру функции с точным указанием типов аргументов и возвращаемого результата. Затем задекларированную функцию можно скомпилировать.

  • result = payload — эта строчка копирует исходный массив. Именно поэтому значения в result теперь можно менять.

Проверим быстродействие:

t5 = RepeatedTiming[mask5[maskingKey, payload1Mb]]
Получилось даже медленнее О_о

Получилось даже медленнее О_о
Играл с типами и проиграл

Играл с типами и проиграл

Что мы видим? Все эти накладные расходы на копирование, преобразования, передачу по значению и изменение значений в копии напрямую сделали код только медленнее! Но зато он эффективнее по использованию памяти.

У этого способа есть еще один минус. Не всякий пользователь сможет эту функцию скомпилировать из-за того что технология появилась относительно недавно и в версиях языка 12, 13 и 14 ведет себя по разному. Где-то код выше в принципе не скомпилируется. Плюс сама по себе компиляция занимает много времени. Конечно результат можно сохранить на диск, но это нужно сделать для всех поддерживаемых операционных систем. В итоге накладные расходы и сложность поддержки экспериментальной функции имею свои минусы и я продолжил поиск путей оптимизации.

LibraryLink

Как известно, ядро Wolfram Language написано на Си. Плюс многие критически важные функции тоже реализованы на Си. Можно ли самостоятельно загрузить в ядро такую функцию? Конечно да, это делается при помощи встроенной библиотеки LibraryLink. Она предоставляет функции для компиляции и использования кода на языке Си прямо в WL.

Как выглядит работа с ней:

  • Пишем код на Си

  • Оборачиваем его в специальные инструкции из библиотеки «WolframLibrary.h»

  • На стороне WL компилируем при помощи CreateLibrary

  • И загружаем при помощи LibraryFunctionLoad

  • Готово!

Но лучше один раз увидеть… Далее будет часть кода на Си.

В каждой библиотеке на языке Си необходимо всегда выполнять ритуал импорта вот такой:

#include "WolframLibrary.h" #include "WolframNumericArrayLibrary.h"

Затем для ядра WL необходимо определить три функции, которые инициализируют библиотеку, деинициализируют и возвращают версию LibraryLink, под которую библиотека была скомпилирована:

DLLEXPORT mint WolframLibrary_getVersion() {     return WolframLibraryVersion; }  DLLEXPORT int WolframLibrary_initialize(WolframLibraryData libData) {     return LIBRARY_NO_ERROR; }  DLLEXPORT void WolframLibrary_uninitialize(WolframLibraryData libData) {     return; }

Есть еще небольшая тонкость для работы этой библиотеки на всех ОС. Надо добавить зависимости и одну инструкцию вот так:

#include <stdio.h> #include <stdlib.h> #include <string.h>  #define BYTE uint8_t
Весь ритуальный код на одном скриншоте

Весь ритуальный код на одном скриншоте

Все готово. Теперь приступим к написанию основного кода.

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

DLLEXPORT int mask6(WolframLibraryData libData, mint Argc, MArgument *Args, MArgument Res) {     //get payload and length     MNumericArray nMask = MArgument_getMNumericArray(Args[0]);     BYTE* mask = (BYTE *)libData->numericarrayLibraryFunctions->MNumericArray_getData(nMask);     mint maskLen = MArgument_getInteger(Args[1]);          //get maskingKey and length     MNumericArray nArr = MArgument_getMNumericArray(Args[2]);     BYTE* arr = (BYTE *)libData->numericarrayLibraryFunctions->MNumericArray_getData(nArr);      mint arrLen = MArgument_getInteger(Args[3]);      //create new result array     BYTE *result = (BYTE*)malloc(sizeof(BYTE) * arrLen);       //bit xor     for (size_t i = 0; i < arrLen; i++) {         result[i] = arr[i] ^ mask[i % maskLen];      }          //creating a struct for sending to WL side     MNumericArray nResult;     libData->numericarrayLibraryFunctions->MNumericArray_new(MNumericArray_Type_UBit8, 1, &arrLen, &nResult);     memcpy((uint8_t*)libData->numericarrayLibraryFunctions->MNumericArray_getData(nResult), result, arrLen);      //return result and free memory     MArgument_setMNumericArray(Res, nResult);     free(result);      return LIBRARY_NO_ERROR; }
Дубликат картинкой

Дубликат картинкой

Важно отметить, что:

  • Все функции подключаемые к ядру WL должны иметь аргумент и возвращаемые результат как указано выше.

  • libData — структура с набором функций для связи с ядром

  • Argc — число аргументов на стороне WL

  • Args — сами аргументы

  • Res — структура в которую записывается результат — то, что вернется на стороне WL

  • На стороне Си возвращается код ошибки

Вернемся обратно в блокнот и скомпилируем этот исходник вот так:

Get["CCompilerDriver`"];  Get["LibraryLink`"];   lib = CreateLibrary[File["internal.c"], "internal"]; 

В lib сохранился путь к dll-файлу с машинным кодом, специфичным для ОС. Загрузить из него созданную функцию можно вот так:

cmask6 = LibraryFunctionLoad[ lib,  "mask6",  {"ByteArray", Integer, "ByteArray", Integer},  "ByteArray" ]

А затем создаем более удобное определение, вид которого совпадет с предыдущими вариантами:

mask6[maskingKey_ByteArray, payload_ByteArray] :=  cmask6[ maskingKey, Length[maskingKey],  payload, Length[payload] ]

И измерим время:

t6 = RepeatedTiming[mask6[maskingKey, payload1Mb]]
Стало только хуже, но это пока

Стало только хуже, но это пока

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

Я приведу сразу весь список улучшений, которые можно сделать:

  • Можно избавится от i % maskLen используя двойной цикл

int k;  int len = arrLen - arrLen % maskLen;  for (size_t i = 0; i < len; i = i + maskLen) {   for (size_t j = 0; j < maskLen; j++) {     k = i + j;      result[k] = arr[k] ^ mask[j];    } }  for (size_t i = len; i < arrLen; i++) {   result[i] = arr[i] ^ mask[i % maskLen]; }  /* вместо for (size_t i = 0; i < arrLen; i++) {   result[i] = arr[i] ^ mask[i % maskLen];  }*/

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

  • Далее если внимательно изучить руководство по LibraryLink, то можно понять, что создание result — лишняя операция и тем более копирование результата в итоговый массив. Дело в том, что у любого созданного массива (MNumericArray()) можно извлечь указатель на сам участок памяти где хранятся сырые данные в виде массива чисел. Поэтому можно его и использовать в качестве result:

MNumericArray nResult; libData->numericarrayLibraryFunctions->MNumericArray_new(MNumericArray_Type_UBit8, 1, &arrLen, &nResult); BYTE *result = (BYTE*)libData->numericarrayLibraryFunctions->MNumericArray_getData(nResult);   /* вместо BYTE *result = (BYTE*)malloc(sizeof(BYTE) * arrLen);  MNumericArray nResult; libData->numericarrayLibraryFunctions->MNumericArray_new(MNumericArray_Type_UBit8, 1, &arrLen, &nResult); memcpy((uint8_t*)libData->numericarrayLibraryFunctions->MNumericArray_getData(nResult), result, arrLen); free(result);*/

Итоговый код на Си будет вот такой:

DLLEXPORT int mask7(WolframLibraryData libData, mint Argc, MArgument *Args, MArgument Res) {     MNumericArray nMask = MArgument_getMNumericArray(Args[0]);     BYTE* mask = (BYTE *)libData->numericarrayLibraryFunctions->MNumericArray_getData(nMask);     mint maskLen = MArgument_getInteger(Args[1]);          MNumericArray nArr = MArgument_getMNumericArray(Args[2]);     BYTE* arr = (BYTE *)libData->numericarrayLibraryFunctions->MNumericArray_getData(nArr);      mint arrLen = MArgument_getInteger(Args[3]);      MNumericArray nResult;     libData->numericarrayLibraryFunctions->MNumericArray_new(MNumericArray_Type_UBit8, 1, &arrLen, &nResult);     BYTE *result = (BYTE*)libData->numericarrayLibraryFunctions->MNumericArray_getData(nResult);       const int len = arrLen - arrLen % maskLen;      int k;       for (size_t i = 0; i < len; i = i + maskLen) {         for (size_t j = 0; j < maskLen; j++) {             k = i + j;              result[k] = arr[k] ^ mask[j];          }     }      for (size_t i = len; i < arrLen; i++) {         result[i] = arr[i] ^ mask[i % maskLen];     }      MArgument_setMNumericArray(Res, nResult);     return LIBRARY_NO_ERROR; }
  • И последнее, но уже на стороне WL. Каждый раз при вызове библиотечной функции массив байт будет копировать из памяти, которая доступна ядру в ту память, которая доступна библиотечной функции. С одной стороны это более безопасно, так как даже если внутри библиотечной функции изменить входной массив, то это не повлияет на то, что происходит в сессии ядра. С другой стороны — копирование занимает время. Но у LibraryLink есть механизм использования общей памяти для аргументов. Это значит, что копирования не будет, а будет передача ссылки на уже существующий в сессии массив. Платой за это является то, что попытки поменять значения в общем массиве или очистить его приводят к падению ядра. В общем использовать этот механизм можно вот так:

cmask7 = LibraryFunctionLoad[   lib,    "mask7",    {     "ByteArray",      Integer,      {LibraryDataType["ByteArray"], "Shared"},      Integer   },    "ByteArray" ]

Ну а далее все как обычно:

mask7[maskingKey_ByteArray, payload_ByteArray] :=  cmask7[ maskingKey, Length[maskingKey],  payload, Length[payload] ]  t7 = RepeatedTiming[mask7[maskingKey, payload1Mb]]

Посмотрим, что получилось!

Самый эффективный на данный момент вариант

Самый эффективный на данный момент вариант

Итоговая разница в скорости выполнения с самой первой наивной реализацией в 2550 раз, а с самой быстрой предыдущей реализацией, скомпилированной для LLVM в 5.8 раз. В итоге теперь данные размером в 1 Мб маскируются и демаскируются примерно за 666 мкс, что как бы намекает…

Киану готовится изгонять компилятор из моего ноутбука

Киану готовится изгонять компилятор из моего ноутбука

Выводы

Еще раз повторюсь, что в первую очередь пытался продемонстрировать подход к написанию эффективного кода для Wolfram Language. Да, в итоге я обратился к Си, но и этот способ имеет право на жизнь и часто нами используется в различных библиотеках. Подтвердилось ли, что WL — медленный язык? И да и нет. Если писать код бездумно — то уже ничего не поможет, если же погрузиться в WL дальше уровня решения уравнений и взятия интегралов и использовать его как язык программирования — то всегда можно найти эффективное решение для многих задач. Всем спасибо за внимание!


ссылка на оригинал статьи https://habr.com/ru/articles/817563/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *