Как стать автором
Обновить

Комментарии 77

А вы страхуете себя от ошибок строгими типами?

Запретом на reinterpret_cast.


Вот весь пост — прямо классическая иллюстрация причины, скрывающейся за правилом: никогда не парсить бинарные данные с помощью приведения к типу указателя на __attribute__((packed)) структурки.


Кроме возможных проблем с порядком байтов, которые рассматриваются здесь, подобные махинации ещё могут нарушать требования к выравниванию типов. Естественно, всё это работает на x86 и современных ARM, потому что производители железа сдались. Но формально нарушение выравнивания является undefined behavior, и существуют архитектуры, где это фатально.

никогда не парсить бинарные данные с помощью приведения к типу указателя на attribute((packed)) структурки.

полностью согласен. без memcpy никуда...

никогда не парсить бинарные данные с помощью приведения к типу указателя на __attribute__((packed)) структурки.

А как тогда? Героически memcpy'ить каждое поле? Так это кода в несколько раз больше. И вот совсем не факт, что ошибок от этого станет меньше.

махинации ещё могут нарушать требования к выравниванию типов

Нет. Для этих «старых» архитектур компилятор для полей упакованных структур генерирует код, который читает их побайтно, а потом склеивает. Проверено на ARM7, который падает в data abort на unaligned access.
Каждое поле memcpy'ить не нужно, но и делать reinterpret_cast какого-нибудь произвольного куска буфера char* на указатель на упакованную структуру тоже нельзя. Нужно выделить память именно под данную структуру (скажем, на стеке), тогда требования к выравниванию структуры в целом (а они обычно у компилятора есть даже для упакованных структур) будут соблюдены, и заmemcpy'ить в нее вышеупомянутый кусок буфера, ну и дальше просто работать с ней напрямую (а вот брать а затем разыменовывать указатели на конкретные поля упакованных структур в общем случае нельзя).
тогда требования к выравниванию структуры в целом (а они обычно у компилятора есть даже для упакованных структур)

Я всегда считал, что у упакованных структур нет требований к выравниванию. В этом, в общем-то, и весь их смысл. Если есть сомнения/паранойя, можно вставить в код
static_assert(__alignof__(MyStruct) == 1);
перед тем, как делать reinterpret_cast.

Я бы не стал на это ставить. Компилятор может разместить её в памяти так, что одни её поля будут выравнены, а другие — нет, и генерировать специальный код только для доступа к тем полям, которые по его мнению не выравнены. Смысл упакованных структур все-таки не в том, чтобы можно было чихать на выравнивание ЛЮБЫХ полей, а в том, чтобы между полями не вставлялся паддинг.

Согласен, что эта особенность плохо документирована, но все-таки ставить на это можно. При наличии сомнений можно использовать такую обертку:
template<typename T>
T* unaligned_cast(void* p) {
    static_assert(__alignof__(T) == 1);
    return reinterpret_cast<T*>(p);
}

Даже если и так, я не вижу в правилах применения reinterpret_cast такого случая, чтобы можно было безопасно выполнять такую конвертацию. Можно безопасно приводить (а затем безопасно разыменовывать) object pointer type к char*, но не наоборот.
Хотелось бы подробную статью со всеми подводными камнями и how to по данной теме
Статей про алиасинг (а тут по сути речь о нем) в C и C++ по-моему уже миллион был на хабре, смысл писать миллион первую? :) Вот например:

habr.com/ru/company/otus/blog/442554
А как тогда? Героически memcpy'ить каждое поле?

Именно. Держать мух отдельно от котлет. Бинарное on-the-wire представление — это отдельный формат. Удобный для языка программирования объект — это другой формат. Как абстрактный тип данных «строка» и её конкретное представление, скажем, в UTF-8.


Так как в этой программе TCP-заголовок — это достаточно важный объект, то вполне есть смысл вложиться в написание удобной абстракции над ним, раз уж в стандартой библиотеке её нет.


Для этих «старых» архитектур компилятор для полей упакованных структур генерирует код, который читает их побайтно, а потом склеивает.

Хех, действительно, именно так и делает. Не знал, спасибо!


Просто я травмирован undefined behavior sanitizer, который несмотря на всё это выдаёт предупреждения, когда так пытаешься делать.

Просто я травмирован undefined behavior sanitizer, который несмотря на всё это выдаёт предупреждения, когда так пытаешься делать.

Возможно, в вашем случае он не так уж и неправ. А вы попробуйте вместо каста, который вы по ссылке используете, сделать экземпляр упакованной структуры на стеке, заmemcpy'ить в нее целиком содержимое и потом с ней поработать. Скорее всего, никаких возражений у ub sanitizer'а это не вызовет.

При доступе к полям структур с attribute packed компилятор гарантирует генерацию кода, который будет корректно работать в том числе с невыровненными полями. Но есть нюанс — эти гарантии распространяются только на прямой доступ. Если вы, скажем, возьмёте указатель на конкретное невыровненное поле, то доступ по этому указателю будет уже по "обычным" правилам.

Но формально нарушение выравнивания является undefined behavior, и существуют архитектуры, где это фатально.
Здесь нет никакого нарушения выравнивания. У упакованных структур выравнивание всегда равно единице, поэтому нарушить его невозможно. В этом весь смысл упакованных структур.

Работает это не только на x86 и ARM. Если платформа не поддерживает невыровненные обращения к памяти, то для чтения/записи поля из упакованной стрктуры компилятор вставит соответствующие инструкции.

Вот весь пост — прямо классическая иллюстрация причины, скрывающейся за правилом: никогда не парсить бинарные данные с помощью приведения к типу указателя на __attribute__((packed)) структурки.
Приведенное вами «правило» необоснованно по изложенной выше причине. Упакованные структуры — весьма практичный способ разбора бинарных данных.

У меня протокол поверх TCP, но нет, ничего такого не делаю.
Со всеми полями работаю в host ordering, так что нет необходимости держать
заголовок в двух представлениях(BE и LE) — сразу декодирую по приходу.


А так да, — ваше решение хорошо иллюстрирует пример безопасного кодинга со строгой типизацией.

Убрать весь такой код подальше в общие библиотеки и покрыть тестами. Полностью покрыть. С кодревью тестов.

Переписывать как можно реже. Желательно вообще никогда.

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

Пришел к использованию типизации везде, где только возможно. Вплоть до того что чистых int или short практически нет, всегда можно придумать смысл для типа переменной.
Немного расстраивает невозможность простого определения своих типов из базовых, которые было бы нельзя мешать с другими, ну типа
typedef uint wheels_count;
typedef uint doors_count;
doors_count y= 2;
wheels_count x= y;// Вот это хотелось бы сделать невозможным на уровне компилятора.

Советую посмотреть на библиотеку NamedType.

Если использовать статический анализатор, типа Lint, с правилами проверки строгой типизации, то он руганется на этом коде.
Причём на двух строчках сразу, так как 2 литерал int и происходит неявное преобразование 2 к doors_count, aka unsigned int.
Я себе написал простой шаблонный класс. Назвал like.
Пользуюсь так
struct wheel_count : like<uint>;

Внутри есть автоматический оператор типа, поэтому получение uint'а прозрачно для программиста, а присвоение другого like<> типа невозможно.
А зачем наследование? Можно же как alias использовать using wheel_count = like<int>;
Или в структуре wheel_count ещё что-то определено?

Если будет несколько like — то они между собой отличаться же не будут в вашем случае. А если заводить по отдельной структуре на каждый тип — то компилятор будет следить.

Да, всё именно так. Может кто то знает более выразительный способ?

В библиотеке, на которую я выше приводил ссылку, для этого дополнительный шаблонный параметр заведён.
NamedType<int, struct wheels_count_tag> и NamedType<int, struct doors_count_tag> — уже разные типы.

Понял, спасибо.
а можно его тут привести? а то попробовал сам изобразить — что-то бойлер-плейта много получается.
Пишу по памяти:
template<typename LikeWhat>
struct like
{
    LikeWhat value;

    operator LikeWhat()
    {
        return value;
    }

    void assign( const LikeWhat& newVal)
    {
        value = newVal;
    }
};

Из минусов, нету оператора присваивания и конструктора из исходного типа (они никак не смогут подтянуться из базового типа). Если они нужны, то прийдется применить ещё одну абстракцию что бы не писать бойлерплейт для них (а внутренний тип назвать impl например).

template<typename T>
struct assignable: T
{
    assignable(const decltype (T::value)& arg)
    {
        T::value = arg;
    }

    assignable& operator=(const decltype (T::value)& arg)
    {
        T::value = arg;
        return *this;
    }
};


тогда код будет таким:
struct wheels_count_impl : like<uint> {};
using wheels_count = assignable<wheels_count_impl>;
struct doors_count : like<uint> {};


int main(int argc, char *argv[])
{
    wheels_count wc(3);
    wc = 2;

// или
    doors_count dc;
    dc.assign( 4 );
Из минусов, нету оператора присваивания и конструктора из исходного типа (они никак не смогут подтянуться из базового типа)

Почему же?


struct like
{
    LikeWhat value;

    like(const LikeWhat& value)
        : value(value) {}

    operator LikeWhat()
    {
        return value;
    }

    void assign(const LikeWhat& newVal)
    {
        value = newVal;
    }
};

struct wheels_count : like<int>
{
    using like<int>::like;
};

void Test2()
{
    wheels_count wc = 3;
    wc = 4;
}

С другой стороны, имеем неявное преобразование типа в обе стороны — а это может негативно сказаться на самом смысле использования строгой типизации. Проблема решается explicit конструктором:


template<typename LikeWhat>
struct like
{
    LikeWhat value;

    explicit like(const LikeWhat& value)
        : value(value) {}

    operator LikeWhat()
    {
        return value;
    }

    void assign(const LikeWhat& newVal)
    {
        value = newVal;
    }
};

struct wheels_count : like<int>
{
    using like<int>::like;
};

void Test2()
{
    auto wc = wheels_count(3);
    wc = wheels_count(4);
}
Да, using может протянуть область видимости, но это уже получается «клиентский бойлерплейт» хоть и небольшой, но от которого всё ещё хочется избавиться.
С моей точки зрения идеальное решение должно быть полностью скрыто за фасадом шаблона или базового класса или порождающего паттерна.
НЛО прилетело и опубликовало эту надпись здесь
А чтобы потом ошибок не было,
auto cash = 1000;
....
cash += 32000;

На каком нибудь 16 битном или 8 битном процессоре и его компиляторе.
Вы удивитесь, что ваш cash стал «немножко» не тем.
А все потому что компилятор вывел за вас тип, в данном случае int, размер которого на компиляторах для 16 и 8 битных микроконтроллеров очень даже может быть 16 бит.

Поэтому тип важен и лучше его явно указывать.
uint32_t сash  = {1000U};

Auto полезно для выведения сложных типов, шаблонных классов или возврата функций, которые компилятор неявно не преобразует к чему-то ещё. А простые явно лучше указывать.
НЛО прилетело и опубликовало эту надпись здесь
1.
Я не понимаю каким образом этот пример вообще коррелирует с моими словами. Я где-то сказал что типы не важны?

Возможно я вас неправильно понял, но
Имена переменных важнее их типов.
воспринялось мной именно так. Даже просто сравнение, что одно важнее другого наводит на мысль, что типы не так важны как имена, хотя это совершенно не так, так как из-за имен навряд ли вы ошибку получите, а вот из-за типов получите. Достаточно вспомнить ошибку преобразования из одного типа в другой в ПО для Ариант-5, даже на таком строго типизированном языке как Ada, где неявные преобразования запрещены почти для всех типов.

2. Я имел ввиду, что для простых типов (которые компилятор неявно может преобразовать) тип не то, что хочешь или не хочешь — указывай, а необходимо указывать, иначе можно нарваться на неприятность.

3. Под словом алиасы, вы наверное имели ввиду обертки над типами. Да придется скорее всего операторы у них переопределять. Либо как выше показано, И кастовать придется явно в таком случае, чтобы дать понять компилятору, что это ваша задумка и вы это делаете осознано, впрочем, даже явные касты — это не хорошо.

4. Это «не сужающая» инициализация. Если вы не используете статический анализатор кода, то, в случае если вы напишете int f = 3.14 ;, компилятор сделает неявное преобразование double к int, и собственно не обязан вам об этом сообщать, хотя думаю, что современные компиляторы что-то вам скажут, но не все. Но на вот такое int f = {3.14} ; компилятор обязан выдать вам ошибку, ну или как минимум warning, что-то типа invalid narrow conversion. Скажет, что вы 3.14 aka double «обрезали» до типа f aka int, т.е. потеряли точность. В данном случае в uint32_t сash = {1000U}; это не особо пригодится, но, например, в таком случае int cash = {40 000} ; это будет очень даже будет осмысленно. Так как если int вдруг окажется 16 битным, то тут будет ошибка narrow conversion. Я уже просто так привык писать везде.

3.
Добавлю ещё, что переопределение операторов для таких типов несёт дополнительный смысл: можно запретить операции, не имеющие смысла. Например, запретить умножать width на width, но разрешить умножать width на height, причём результатом такого умножения будет ещё один тип area.


4.
А почему не просто uint32_t сash{1000U};?

3. Ага, точно

4.
uint32_t сash{1000U};

Да, можно использовать прямую инициализацию, для меня привычнее просто делать это копирующей инициализацией в случае с простыми типами.
Осталось перейти на использование контрактного программирования и соответствующие языки (Eiffel или SPARK+Ada).
(это была отчасти шутка)

Скорее в статье говорится про статическую типизацю.

Нет: https://en.wikipedia.org/wiki/Strong_and_weak_typing.
C++ и так статически типизированный. Речь именно о запрете некоторых неявных преобразований.

Мне почему-то при чтении статьи показалось, что сначала оперировали int, а потом добавили отдельные типы данных.

Лучший вариант решения этой проблемы (из всех видимых мной) реализован на чистом C в ядре Linux. Вполне допускаю что не только в Linux, но и в других UNIX. Типы __le16 и __be16, а так же __le32 и __be32 совершенно четко избавляют от описанных проблем. Просто в нужных структурах используются нужные типы. Вот только с переносимостью такого решения (особенно между разными компиляторами на разных платформах) есть проблемы.
Полностью поддерживаю идею о борьбе с багами таким способом с помощью строгой типизации.

Думаю, тот же подход можно применить и к борьбе с SQL-инъекциями, например (если код, обращающийся к SQL написан на C++, конечно). То есть если мы (условно) пишем
bool ok = check_for_special_characters (input);
if (ok) 
{
std::string sql_request = std::string("SELECT ") + input;
...

или
std::string sql_request = std::string("SELECT ") + escape(input);
, то компилятор не может это отличить от
std::string sql_request = std::string("SELECT ") + input;

А если бы у нас было 2 разных типа class SqlRequestString и std::string, то компилятор ругался бы на их тупую конкатенацию.
Для sql уже есть готовое решение всех этих проблем и оно лежит в другой плоскости. Надо использовать prepared statements, а не конкатенировать строки.
Мне кажется, это не совсем «в другой плоскости», класс PDOStatement из php по защитной функциональности аналогичен гипотетическому классу SqlRequestString из моего комментария (я не претендую на то, что первым придумал эту идею, на мой взгляд, идея такой защиты достаточно очевидна).

И да, это относится не только к SQL, но и к другим языкам запроса/программирования, я не знаю, для каких из них реализованы подобные защитные классы, но хотелось бы, чтобы их было побольше.
Мне кажется, это не совсем «в другой плоскости», класс PDOStatement из php по защитной функциональности аналогичен гипотетическому классу SqlRequestString из моего комментария
Это проблема конкретно PDOStatement из php, который только эмулирует prepared statements. Ваш класс из примера всего-лишь добавляет простую проверку на тип. При этом вполне может возникнуть ошибка или уязвимость в функции escape, или пользователь класса что-то не поймет и силой преобразует строку в требуемый тип. А при создании трушного prepared statement в принципе не нужен escape, потому что забинденные к нему параметры никогда не участвуют в парсинге уровня sql-statement, ни на одном из этапов. Код запроса и параметры отделены друг от друга, и в таком виде принимаются СУБД. Это значит, что SQL-Injection через prepared statement физически невозможен, это примерно то же самое что пытаться заразить компьютер вирусом в плейн-текстовом файле.

Увы, это не является решением проблемы. Что будете делать, если число параметров будет исчисляться тысячами?

Переписывать SQL-выражение.

Ну-ка расскажите, как вы будете переписывать INSERT с большим числом записей или SELECT с IN (тут большое множество), желательно платформенно-независимым способом.

Ну-ка расскажите, как вы будете переписывать INSERT с большим числом записей
Как одну транзакцию с многократно выполненным переиспользуемым prepared statement с INSERT. И это даже будет быстрее работать, потому что распарсить маленький запрос и подставлять готовые строки данных намного проще чем сначала отэскейпить и сконкатенировать дикую ораву текста (привет, аллокации!), а потом еще ее парсить.
SELECT с IN (тут большое множество)
IN поддерживает подзапросы в качестве аргументов. Очевидное решение — перенести параметры в таблицу.

SQL запросы с тысячами параметров это почти прямой аналог функций с тысячами аргументов. Это же очевиднейший признак проблемы в коде, так делать нельзя нигде и никогда.
Как одну транзакцию с многократно выполненным переиспользуемым prepared statement с INSERT.

Это банально долго, особенно если записей десятки тысяч.
Конечно, можно использовать специфичные для каждой БД команды типа COPY FROM BINARY в случае PostgreSQL — будет эффективно, но непереносимо.


IN поддерживает подзапросы в качестве аргументов. Очевидное решение — перенести параметры в таблицу.

Да, так делать можно и даже нужно. Но что делать, если возможен одновременный доступ многих клиентов к базе? Придётся создавать короткоживущую временную таблицу, но упс, SQL-синтаксис для каждой БД будет уже свой — снова теряем в переносимости.


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

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

Это банально долго, особенно если записей десятки тысяч.
Нет. Как я уже дополнил выше, это будет работать быстрее, на некоторых базах (например, sqlite) — значительно.
Но что делать, если возможен одновременный доступ многих клиентов к базе?
Эмм, а что нужно делать? Или вы думаете что очередная туча эскейпов, конкатенаций, и парсинга будет быстрее чем закешированный и оптимизированный СУБД поиск IN по таблице? Если да, то вы опять же ошибаетесь.
Придётся создавать короткоживущую временную таблицу
Т.е. тысячи параметров еще и разные на каждый запрос? Ну тогда тут нужно искать ошибку на уровне архитектуры. Такой необходимости не должно возникать в 99.99% случаев.
Аналог функции — это prepared statement, а не SQL-запрос. Потому prepared statement с тысячами пароаметров — это бред.
Prepared statement это не более чем предраспарсенный SQL-запрос.
Нет. Как я уже дополнил выше, это будет работать быстрее, на некоторых базах (например, sqlite) — значительно.

Зависит от конкретной ситуации. Если база удалённая, то будет весьма долго и тоскливо вставлять по 1 записи — уже сталкивался с таким.


Эмм, а что нужно делать? Или вы думаете что очередная туча эскейпов, конкатенаций, и парсинга будет быстрее чем закешированный и оптимизированный СУБД поиск IN по таблице? Если да, то вы опять же ошибаетесь.

Опять же зависит от ситуации. Парсинг команды относительно её выполнения во многих случаях занимает пренебрежительно мало времени.


Т.е. тысячи параметров еще и разные на каждый запрос? Ну тогда тут нужно искать ошибку на уровне архитектуры. Такой необходимости не должно возникать в 99.99% случаев.

Конечно. Пример: фильтр огромного лога по параметрам.


Prepared statement это не более чем предраспарсенный SQL-запрос.

Да, с указанием мест, куда будут помещены параметры.

Если база удалённая, то будет весьма долго и тоскливо вставлять по 1 записи — уже сталкивался с таким.
Не будет конечно. Оно отошлет все пачкой, если только вы не забыли про упомянутое мной оборачивание в транзакцию.
Парсинг команды относительно её выполнения во многих случаях занимает пренебрежительно мало времени.
Будет быстрее в любом случае. Выполнение есть и там и там. А вот поверх этого эйскейп, конкатенация, и парсинг только во втором случае. Нельзя выполнить работу быстрее чем ее же и еще несколько других.

Более того, парсинг списка это линейная сложность алгоритма в лучшем случае, а поиск в этом списке — в худшем. Обычно тут возникнет либо быстрый поиск по индексу, либо фулл скан который в среднем будет останавливаться просмотрев половину списка (при равномерном распределении вероятности найти элемент).
Пример: фильтр огромного лога по параметрам.
Извините, но можно подробней, что это за фильтр которому понадобилились тысячи параметров (да таких, что их нельзя свести к запросу по базе), и насколько подобная операция является повседневной и типовой, чтобы утверждать что «prepared statements не является решением проблемы»?
Да, с указанием мест, куда будут помещены параметры.
Что не меняет сути. Эти параметры можно заинлайнить. Так же как можно изменить api СУБД чтобы она принимала параметры в функцию выполнения SQL вместе со строкой с запросом. Ну т.е. они ортогональны друг другу по природе.
Не будет конечно. Оно отошлет все пачкой, если только вы не забыли про упомянутое мной оборачивание в транзакцию.

Можете привести пример? Я не нашёл функций в стандартных API, которые позволяют так делать. Только методы типа ExecuteNonQuery, но это вызов команд по очереди с ожиданием ответа. И транзакции тут вообще не при чём.


Нашёл только addBatch в Java, но т.к. я Java не пользуюсь, я не могу сказать, что там происходит под капотом.


Извините, но можно подробней, что это за фильтр которому понадобилились тысячи параметров (да таких, что их нельзя свести к запросу по базе), и насколько подобная операция является повседневной и типовой, чтобы утверждать что «prepared statements не является решением проблемы»?

Операция достаточно редкая, чтобы усложнять логику запросов, создавая временные таблицы и prepared statements.


Если рассматривать упрощённую модель, то лог — это набор записей с привязкой к ID объектов. Объектов много, и иногда требуется сделать фильтр по набору объектов, который определяется массивом ID.

Можете привести пример? Я не нашёл функций в стандартных API, которые позволяют так делать.
Так никаких хитрых функций и не нужно (хотя возможно какие-то СУБД предоставляют соответствующие настройки). Просто начинаете транзакцию, делаете кучу инсертов, и заканчиваете транзакцию. Современные СУБД достаточно умны чтобы кешировать данные на запись и отправлять их крупными пачками.

Еще можно вспомнить специальный механизм Bulk Insert, но если нужна поддержка множества СУБД о ней можно сразу же и забыть.
Если рассматривать упрощённую модель, то лог — это набор записей с привязкой к ID объектов. Объектов много, и иногда требуется сделать фильтр по набору объектов, который определяется массивом ID.
Допустим. Вряд ли многотысячный список ID программист создает ручками каждый раз с нуля. Он либо генерируется по каким-то критериям, либо хранится и периодически пополняется со временем. В первом случае можно добавить необходимые критерии в базу, и делать по ним запрос. Во втором случае можно перенести хранение списка фильтруемых ID в базу.

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

Так это и будет медленно. Пусть база и кэширует данные на запись — это её дело, но бутылочным горлышком здесь может оказаться сетевой интерфейс между базой и клиентом (а точнее, latency). Каждый инсерт — это отправка команды по сети и ожидание ответа.


Он либо генерируется по каким-то критериям, либо хранится и периодически пополняется со временем.

Да, он генерится на стороне пользовательского интерфейса. Но класть его в базу нет никакого смысла.

Каждый инсерт — это отправка команды по сети и ожидание ответа.

В случае транзакций, это работает по другому: ответ будет получен только если произойдёт ошибка во время вставок, либо по окончании и подтверждении транзакции. Транзакция — или всё или ничего.

Ответ не зависит от того, находитесь ли вы внутри транзакции или нет. Внутри транзакции вы видите состояние базы так, как если бы был автокоммит.


А так да, транзакции — это про фиксацию изменений в базе (всё или ничего) и изоляцию от других сессий.

Каждый инсерт — это отправка команды по сети и ожидание ответа
В том то и дело, что нет, если транзакции использяются явно, или выключен режим autocommit.
В смысле «нет»? Даже в рамках транзакции в приложении может работать логика, основанная на результатах выполнения каждого конкретного запроса, в том числе INSERT. Например, в postgresql ты можешь выполнить INSERT, потом через CmdTuples() посмотреть количество affected rows, а потом на основании этого принять решение, делать тебе следующий INSERT, COMMIT или вообще ROLLBACK. Но для этого, естественно, этот первый INSERT надо сначала таки выполнить.

Возможно, конечно, в клиентской библиотеке может быть какая-то эвристика, которая будет пытаться как-то «пакетировать» запросы при каких-то условиях, но в общем случае пакетирования не будет.
Само собой, если в транзакции есть логика, требующая что-то сделать на основе данных, то клиенту придется послать команды на выполнение. Так что да, в общем случае не будет. Но если клиент просто делает тысячи плейн-инсертов подряд в одну таблицу, то это совсем другой случай.
Ну так а откуда клиентская библиотека знает, так сказать, когда какой случай? Вот DistortNeo у вас и пытается узнать, о какой конкретно СУБД и клиентской библиотеке речь и как там включается режим пакетирования.
Ну так а откуда клиентская библиотека знает, так сказать, когда какой случай?
Cкорость отправки по сети на много порядков (3-4 вроде) ниже чем скорость с которой данные будут приходить от prepared statement. Поэтому если вызывать достаточно простых insert подряд, они будут банально копиться в очереди, естественным образом. Ожидания результатов нет, очередь на запись большая — пакеты готовы.

А если клиент что-то делает в зависимости от результата, он просто не вызовет следующий prepared statement, пока не дождется его, и очередь не будет пополняться.
о какой конкретно СУБД и клиентской библиотеке речь
Думаю, это так или иначе применимо для большинства популярных СУБД.

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

Ну а вы можете привести какую-то конкретику — что за клиент и к какой СУБД работает таким образом? Postgres'овская libpq например работает не так — там каждый PQexecPrepared() ждет результата выполнения запроса, ничего ни в какой очереди не копится.
Да, я что-то на ночь глядя тупанул. Подобный финт с батчингом сработает только если все инсерты части одной операции, ну и в либах где оно явно задается (addBatch/executeBatch в JDBC, например). Тем не менее, ускорение при использовании транзакции есть, иногда весьма заметное, что и ввело меня в заблуждение. Возможно просто этап закрытия/открытия транзакций слишком медленный сам по себе.
Просто начинаете транзакцию, делаете кучу инсертов, и заканчиваете транзакцию. Современные СУБД достаточно умны чтобы кешировать данные на запись и отправлять их крупными пачками.

Как такового кеширования нет. Создаётся новая версия строки в таблице, и после коммита (подтверждения записи) они либо появляются в таблице, либо так и исчезают (вроде бы ещё и прихватывая с собой авто-инкрементированные последовательности айдишников; врать не буду, но в теории: если отменить в конце транзакцию на 100 вставок, то следующая запись будет с id: OLD+100+1)


Оттого, что основная часть работы по закреплению данных производится на этапе коммита, то по скорости выходит выгоднее вставки/удаления/обновления в блоке транзакции: или 100 записей за раз распихать по БД + обновить индексы, или 100 раз по одной записи — время затрачиваемое на блокировку таблиц/столбцов/строк (в зависимости от реализации/стратегии блокировок) является узким местом.

Оттого, что основная часть работы по закреплению данных производится на этапе коммита, то по скорости выходит выгоднее вставки/удаления/обновления в блоке транзакции: или 100 записей за раз распихать по БД + обновить индексы, или 100 раз по одной записи — время затрачиваемое на блокировку таблиц/столбцов/строк (в зависимости от реализации/стратегии блокировок) является узким местом.

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


Каждый INSERT — это отправка данных по сетевому соединению и ожидание ответа от базы ("команда получена и выполнена"). Пинг 2 мс при вставке тысячи строк отдельными инсертами выльется минимум в 2-секундную задержку. Если же все данные объединить в один INSERT, то задержка будет не больше 10 мс.


Аналогия: что быстрее — скачать 1000 файлов по 1 кб или скачать 1 файл размером в 1 мб?


Ну а сколько там будет пыжиться база, вставляя эти строки в таблицу, уже отдельное дело.


вроде бы ещё и прихватывая с собой авто-инкрементированные последовательности айдишников; врать не буду, но в теории: если отменить в конце транзакцию на 100 вставок, то следующая запись будет с id: OLD+100+1

Да, авто-инкременты не откатываются.

Если же все данные объединить в один INSERT, то задержка будет не больше 10 мс.

Кроме как сформировать текст запроса с 1000 инсертами и выполнить запрос одним разом, — не вижу способов.
Если вы что-то знаете об этом больше меня — не стесняйтесь, рассказывайте :)

Стоп! Какой текст запроса, если мы говорим об использовании prepared statements, которые как раз и используются для того, чтобы не делать запросы текстом?


Вызов prepared statement — это вообще отдельная команда БД, и параметры там обычно передаются в бинарном виде.


Если вы что-то знаете об этом больше меня — не стесняйтесь, рассказывайте :)

Ну вот в PostgreSQL есть способ бинарного импорта/экспорта, в других БД тоже наверняка имеются варианты. А вот универсального варианта, кроме как городить либо 1000 инсертов (пусть и с prepared statements), либо один большой, нет.

Ну prepared так prepared, сути дела это не меняет.


Вызов prepared statement — это вообще отдельная команда БД, и параметры там обычно передаются в бинарном виде.

Ну эээ, я как бэ знаю что такое prepared statement.


А вот универсального варианта, кроме как городить либо 1000 инсертов (пусть и с prepared statements), либо один большой, нет.

Делайте так, как считаете нужным.

IN поддерживает подзапросы в качестве аргументов. Очевидное решение — перенести параметры в таблицу.

Типовой сценарий. Сверяем табличку со внешним источником.
Табличка большая, внешний источник большой. Для упрощения и там и там одна колонка ID.
Сверка раз в сутки, внешний источник за сутки изменяется непредсказуемо и неконтролируемо. Наша sql табличка в течении суток тоже меняется. Нужны записи которые есть во внешнем источнике и нет в sql табличке. Ожидаемый результат: таких записей вообще нет или их очень мало.

Писать в таблицу дорого. Запись на sql базах никак не масштабируется. Разделить внешние данные на чанки и прогнать кучу IN() быстрее всего.
Когда я работал над реализацией протокола WiFi, мы тоже пришли к похожему решению:

fuchsia.googlesource.com/fuchsia/+/refs/heads/master/src/connectivity/wlan/lib/common/rust/src/big_endian.rs

В основном протоколе все поля в little endian, но иногда попадаются big endian из верхних слоев.

У вас использование bswap() в таком виде, как в статье используется и в реальном коде? Не думали над ситуацией "ARM/PowerPC/MIPS в режиме BE"? Т.е. у вас нет никакой проверки времени компиляции, для какой последовательности байт собираетесь?

Сейчас мы ориентируемся только на x86 + я для статьи низкоуровневые вещи вытащил из отдельного файла ближе к телу.

Понял, просто мне было интересно, если вы делали проверку, как это реализовывали. А то честный способ с возможностью constexpr завезли только в C++20. Остальное или в рантайме, или на макросной магии с компилятор-специфичными декларациями и предположениями.

Привет вас из мира сурового C. Тоже недавно начал строгую типизацию для борьбы с неправильным порядком аргументов при передаче в функцию. Пока велосипед выглядит примерно так:
#define DECLTYPE_ALIAS(alias, base_type) \
    typedef struct { base_type _; } t_##alias; \
    t_##alias alias( base_type const _ );

#define CONSTRUCT_ALIAS(alias, base_type) \
    t_##alias alias( base_type const _ ) { t_##alias Ret; Ret._ = _; return Ret; };

DECLTYPE_ALIAS( kurs, double );
DECLTYPE_ALIAS( tang, double );
DECLTYPE_ALIAS( kren, double );

CONSTRUCT_ALIAS( kurs, double );
CONSTRUCT_ALIAS( tang, double );
CONSTRUCT_ALIAS( kren, double );

t_carrier_angles carrier_angles( t_kurs const Kurs, t_tang const Tang, t_kren const Kren )
{
    t_carrier_angles Ret = {{0},{0},{0}};
    Ret.Kurs = Kurs; Ret.Tang = Tang; Ret.Kren = Kren;
    return Ret;
};

t_carrier_angles const KursKrenTang = carrier_angles(
    kurs( ScenePacket->Carrier.psic_aircraft ),
    tang( ScenePacket->Carrier.tetac_aircraft ),
    kren( ScenePacket->Carrier.tamac_aircraft ) );

По названию последней переменной нетрудно догадаться, что ошибки с неправильным порядком аргументов неизбежны)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории