Как я реализовывал алгоритм маскирования для протокола 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%

И снова внимательно посмотрим на типы данных. На вход передаются массивы байт. Специальная структура данных для хранения чисел ubyte. Она хранит их наиболее эффективно. Но когда мы вызываем функцию 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/


Интересные статьи

Интересные статьи

Краткий обзор курса, который я недавно закончил пилить на степике. Курс хардкорный :) В нем необходимо с нуля писать алгоритмы машинного. Наверное это один из лучший способов досконально разобраться в...
Рано или поздно веб-приложения перерастают среду одного сервера. Компаниям требуется увеличить или их доступность, или масштабируемость, или и то, и другое. Чтобы сделать это, они развёртывают своё ...
В статье рассмотрены генетические алгоритмы, принципы их работы, основные операторы, приведен пример построения генетического алгоритма, формирование исходной популяции, и ее переход в новую популяцию...
Две команды нашли сразу два разных способа для квантовых компьютеров обрабатывать нелинейные системы, представив их в виде линейных.Иногда компьютер может очень легко пре...
В последние 70 лет проектировщики транспортной системы в Америке использовали одну и ту же модель, решая, что лучше построить. Проблема в том, что она часто ошибается. В ноябре 2...