Циклическое отображение файлов снижает производительность

У меня есть кольцевой буфер, который поддерживается файловой памятью (размер буфера составляет от 8 до 512 ГБ).

Я пишу в (8 экземпляров) эту память последовательно, от начала до конца, и в этот момент она возвращается к началу.

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

using namespace boost::interprocess;

class mapping
{
public:

mapping()
{
}

mapping(file_mapping& file, mode_t mode, std::size_t file_size, std::size_t offset, std::size_t size)
: offset_(offset)
, mode_(mode)
{
const auto aligned_size         = page_ceil(size + page_size());
const auto aligned_file_size    = page_floor(file_size);
const auto aligned_file_offset  = page_floor(offset % aligned_file_size);
const auto region1_size         = std::min(aligned_size, aligned_file_size - aligned_file_offset);
const auto region2_size         = aligned_size - region1_size;

if (region2_size)
{
const auto region1_address  = mapped_region(file, read_only, 0, (region1_size + region2_size) * 2).get_address();
const auto region2_address  = reinterpret_cast<char*>(region1_address) + region1_size;

region1_ = mapped_region(file, mode, aligned_file_offset, region1_size, region1_address);
region2_ = mapped_region(file, mode, 0,                   region2_size, region2_address);
}
else
{
region1_ = mapped_region(file, mode, aligned_file_offset, region1_size);
region2_ = mapped_region();
}

size_ = region1_.get_size() + region2_.get_size();
offset_ = aligned_file_offset;
}

auto offset() const   -> std::size_t  { return offset_; }
auto size() const     -> std::size_t  { return size_; }
auto data() const     -> const void*  { return region1_.get_address(); }
auto data()           -> void*        { return region1_.get_address(); }
auto flush(bool async = true) -> void
{
region1_.flush(async);
region2_.flush(async);
}
auto mode() const -> mode_t { return mode_; }

private:
std::size_t   offset_ = 0;
std::size_t   size_ = 0;
mode_t        mode_;
mapped_region region1_;
mapped_region region2_;
};

struct loop_mapping::impl final
{
std::tr2::sys::path         file_path_;
file_mapping                file_mapping_;
std::size_t                 file_size_;
std::size_t                 map_size_     = page_floor(256000000ULL);

std::shared_ptr<mapping>    mapping_ = std::shared_ptr<mapping>(new mapping());
std::shared_ptr<mapping>    prev_mapping_;

bool                        write_;

public:
impl(std::tr2::sys::path path, bool write)
: file_path_(std::move(path))
, file_mapping_(file_path_.string().c_str(), write ? read_write : read_only)
, file_size_(page_floor(std::tr2::sys::file_size(file_path_)))
, write_(write)
{
REQUIRE(file_size_ >= map_size_ * 3);
}

~impl()
{
prev_mapping_.reset();
mapping_.reset();
}

auto data(std::size_t offset, std::size_t size, boost::optional<bool> write_opt) -> void*
{
offset = offset % page_floor(file_size_);

REQUIRE(size < file_size_ - map_size_ * 3);

const auto write = write_opt.get_value_or(write_);

REQUIRE(!write || write_);

if ((write && mapping_->mode() == read_only) || offset < mapping_->offset() || offset + size >= mapping_->offset() + mapping_->size())
{
auto new_mapping = std::make_shared<loop::mapping>(file_mapping_, write ? read_write : read_only, file_size_, page_floor(offset), std::max(size + page_size(), map_size_));

if (mapping_)
mapping_->flush((new_mapping->offset() % file_size_) < (mapping_->offset() % file_size_));

if (prev_mapping_)
prev_mapping_->flush(false);

prev_mapping_ = std::move(mapping_);
mapping_    = std::move(new_mapping);
}

return reinterpret_cast<char*>(mapping_->data()) + offset - mapping_->offset();
}
}

// 8 processes to 8 different files 128GB each.
loop_mapping loop(...);
for (auto n = 0; true; ++n)
{
auto src = get_new_data(5000000/8);
auto dst = loop.data(n * 5000000/8, 5000000/8, true);
std::memcpy(dst, src, 5000000/8); // This becomes very slow after loop around.
std::this_thread::sleep_for(std::chrono::seconds(1));
}

Есть идеи?

Целевая система:

  • 1x 3TB Созвездие Seagate ES.3
  • 2x Xeon E5-2400 (6 ядер, 2,6 ГГц)
  • 6x 8 ГБ DDR3 1600 МГц ECC
  • Windows Server 2012

13

Решение

8 буферов каждый размером от 8 до 512 ГБ в системе с 48 ГБ физической памяти означает, что ваше отображение должно быть заменено. Там нет ничего удивительного.
Проблема, как вы уже отметили сами, заключается в том, что перед возможностью записи на страницу вы сталкиваетесь с ошибкой, и страница читается. Это не происходит при первом запуске, так как просто нулевая страница используемый. Что еще хуже, чтение на страницах снова конкурирует с записью грязных страниц.

Теперь, к сожалению, нет способа рассказать Windows «Я собираюсь переписать это в любом случае», и при этом нет никакого способа заставить диск загружать Ваши вещи быстрее. Тем не менее, вы можете начать передачу раньше (возможно, когда вы 3/4 через буфер).

Windows Server 2012 (который вы используете) поддерживает PrefetchVirtualMemory который является несколько недоделанной заменой POSIX madvise(MADV_WILLNEED),

Это, конечно, не совсем то, что вы хотите сделать, когда вы уже знаете, что вы все равно перезапишете всю страницу памяти (или несколько из них), но это настолько хорошо, насколько вы можете получить. Стоит попробовать в любом случае.

В идеале вы хотели бы сделать что-то вроде разрушительного madvise(MADV_DONTNEED) как реализовано, например под Linux (и я верю, что FreeBSD тоже) непосредственно перед тем, как перезаписать страницу, но я не знаю ни одного способа сделать это под Windows (… если не считать уничтожения представления, сопоставления и сопоставления с нуля, но затем вы выбросить все данные, так что это немного бесполезно).

Даже с предварительной загрузкой рано вы будете еще быть ограниченным пропускной способностью дискового ввода-вывода, но по крайней мере вы можете скрыть задержку.

Другим «очевидным» (но, вероятно, не таким простым) решением было бы сделать потребителя быстрее. Это позволило бы начать с меньшего размера буфера, и даже для огромного буфера это позволило бы сохранить рабочий набор меньшим (как принудительные страницы производителя, так и потребителя в ОЗУ при обращении к ним, поэтому, если потребитель получит доступ к данным с меньшей задержкой после того, как производитель написали их, они оба будут использовать в основном то же самое набор страниц.) Меньшие рабочие наборы легче вписываются в оперативную память.
Но я понимаю, что вы, вероятно, не выбрали буфер на несколько гигабайт без причины.

1

Другие решения

Так как ваш код лишен каких-либо комментариев, заполнен автоматическими переменными, не компилируется как есть, и у меня нет 512 ГБ, доступного на моем ПК для его тестирования, в любом случае, это останется недоумением.

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

Тем не менее, кажется, что вы просите систему форсированного отображения сбросить либо синхронно, либо асинхронно предыдущий блок в зависимости от ваших вычислений таинственного смещения:

mapping_->flush((new_mapping->offset() % file_size_) < (mapping_->offset() % file_size_));

Я предполагаю, что опрокидывание запускает синхронный сброс, который является вероятным виновником внезапного замедления.

Что операционная система делает в этот момент, зависит от реализации надстройки, которая не описана (или, по крайней мере, достаточно очевидна для меня, чтобы получить ее после беглого взгляда на их справочную страницу).
Если boost наполняет ваши 48 Гб памяти нерасширенными страницами, вы наверняка испытаете внезапное и длительное замедление.

По крайней мере, стоит добавить комментарий в ваш код, если эта таинственная строка делает что-то умное и совершенно другое, я полностью пропустил.

1

Если вы можете сохранить отображение памяти с помощью файла подкачки, а не конкретного файла, вы можете использовать MEM_RESET флаг с VirtualAlloc запретить Windows подкачку старого содержимого.

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

1

Я собираюсь предположить, что под словом «Цикл» вы подразумеваете, что RAM переполнена.
Что происходит, так это то, что, пока ОЗУ не заполнится, все, что вам нужно сделать, — это выделить страницу и записать в нее (скорость ОЗУ), после того, как ОЗУ заполнится, каждое выделение страницы превращается в 2 действия:
1. Вы должны написать грязную страницу назад (скорость DISK)
2. и выделить страницу (скорость ОЗУ)

И в худшем случае вы также должны принести страницу из файла на диске (скорость DISK), если вы что-то читаете с него.
Таким образом, вместо того, чтобы работать только со скоростью ОЗУ (распределение страниц), каждое распределение страниц выполняется со скоростью DISK.
Это не происходит с 2×8 ГБ, потому что он достаточно мал, чтобы вся память обоих файлов полностью осталась в ОЗУ.

0

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

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

0
По вопросам рекламы [email protected]