dbms: unification; using huge pages (experimental) [#METR-2944].

This commit is contained in:
Alexey Milovidov 2015-08-16 16:00:22 +03:00
parent 38fa9c8982
commit f0a5ec4736
5 changed files with 163 additions and 101 deletions

View File

@ -0,0 +1,124 @@
#pragma once
#include <malloc.h>
#include <string.h>
#include <sys/mman.h>
#include <DB/Common/MemoryTracker.h>
#include <DB/Core/Exception.h>
#include <DB/Core/ErrorCodes.h>
/** Отвечает за выделение/освобождение памяти. Используется, например, в PODArray, Arena.
* Интерфейс отличается от std::allocator
* - наличием метода realloc, который для больших кусков памяти использует mremap;
* - передачей размера в метод free;
* - наличием аргумента alignment;
*/
class Allocator
{
private:
/** См. комментарий в HashTableAllocator.h
*/
static constexpr size_t MMAP_THRESHOLD = 64 * (1 << 20);
static constexpr size_t HUGE_PAGE_SIZE = 2 * (1 << 20);
static constexpr size_t MMAP_MIN_ALIGNMENT = 4096;
static constexpr size_t MALLOC_MIN_ALIGNMENT = 8;
public:
/// Выделить кусок памяти.
void * alloc(size_t size, size_t alignment = 0)
{
if (current_memory_tracker)
current_memory_tracker->alloc(size);
void * buf;
if (size >= MMAP_THRESHOLD)
{
if (alignment > MMAP_MIN_ALIGNMENT)
throw DB::Exception("Too large alignment: more than page size.", DB::ErrorCodes::BAD_ARGUMENTS);
buf = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (MAP_FAILED == buf)
DB::throwFromErrno("Allocator: Cannot mmap.", DB::ErrorCodes::CANNOT_ALLOCATE_MEMORY);
/// См. комментарий в HashTableAllocator.h
if (size >= HUGE_PAGE_SIZE && 0 != madvise(buf, size, MADV_HUGEPAGE))
DB::throwFromErrno("HashTableAllocator: Cannot madvise with MADV_HUGEPAGE.", DB::ErrorCodes::CANNOT_ALLOCATE_MEMORY);
}
else
{
if (alignment <= MALLOC_MIN_ALIGNMENT)
{
buf = ::malloc(size);
if (nullptr == buf)
DB::throwFromErrno("Allocator: Cannot malloc.", DB::ErrorCodes::CANNOT_ALLOCATE_MEMORY);
}
else
{
buf = nullptr;
int res = posix_memalign(&buf, alignment, size);
if (0 != res)
DB::throwFromErrno("Cannot allocate memory (posix_memalign)", DB::ErrorCodes::CANNOT_ALLOCATE_MEMORY, res);
}
}
return buf;
}
/// Освободить память.
void free(void * buf, size_t size)
{
if (size >= MMAP_THRESHOLD)
{
if (0 != munmap(buf, size))
DB::throwFromErrno("Allocator: Cannot munmap.", DB::ErrorCodes::CANNOT_MUNMAP);
}
else
{
::free(buf);
}
if (current_memory_tracker)
current_memory_tracker->free(size);
}
/** Увеличить размер куска памяти.
* Содержимое старого куска памяти переезжает в начало нового.
* Положение куска памяти может измениться.
*/
void * realloc(void * buf, size_t old_size, size_t new_size, size_t alignment = 0)
{
if (old_size < MMAP_THRESHOLD && new_size < MMAP_THRESHOLD && alignment <= MALLOC_MIN_ALIGNMENT)
{
if (current_memory_tracker)
current_memory_tracker->realloc(old_size, new_size);
buf = ::realloc(buf, new_size);
if (nullptr == buf)
DB::throwFromErrno("Allocator: Cannot realloc.", DB::ErrorCodes::CANNOT_ALLOCATE_MEMORY);
}
else if (old_size >= MMAP_THRESHOLD && new_size >= MMAP_THRESHOLD)
{
if (current_memory_tracker)
current_memory_tracker->realloc(old_size, new_size);
buf = mremap(buf, old_size, new_size, MREMAP_MAYMOVE);
if (MAP_FAILED == buf)
DB::throwFromErrno("Allocator: Cannot mremap.", DB::ErrorCodes::CANNOT_MREMAP);
}
else
{
void * new_buf = alloc(new_size, alignment);
memcpy(new_buf, buf, old_size);
free(buf, old_size);
buf = new_buf;
}
return buf;
}
};

View File

@ -6,7 +6,7 @@
#include <Poco/SharedPtr.h>
#include <Yandex/likely.h>
#include <DB/Common/ProfileEvents.h>
#include <DB/Common/MemoryTracker.h>
#include <DB/Common/Allocator.h>
namespace DB
@ -25,7 +25,7 @@ class Arena
{
private:
/// Непрерывный кусок памяти и указатель на свободное место в нём. Односвязный список.
struct Chunk : private std::allocator<char> /// empty base optimization
struct Chunk : private Allocator /// empty base optimization
{
char * begin;
char * pos;
@ -38,10 +38,7 @@ private:
ProfileEvents::increment(ProfileEvents::ArenaAllocChunks);
ProfileEvents::increment(ProfileEvents::ArenaAllocBytes, size_);
if (current_memory_tracker)
current_memory_tracker->alloc(size_);
begin = allocate(size_);
begin = reinterpret_cast<char *>(Allocator::alloc(size_));
pos = begin;
end = begin + size_;
prev = prev_;
@ -49,10 +46,7 @@ private:
~Chunk()
{
deallocate(begin, size());
if (current_memory_tracker)
current_memory_tracker->free(size());
Allocator::free(begin, size());
if (prev)
delete prev;

View File

@ -19,6 +19,7 @@
/** Общая часть разных хэш-таблиц, отвечающая за выделение/освобождение памяти.
* Отличается от Allocator тем, что зануляет память.
* Используется в качестве параметра шаблона (есть несколько реализаций с таким же интерфейсом).
*/
class HashTableAllocator
@ -33,9 +34,9 @@ private:
* Рассчитываем, что набор операций mmap/что-то сделать/mremap может выполняться всего лишь около 1000 раз в секунду.
*
* PS. Также это требуется, потому что tcmalloc не может выделить кусок памяти больше 16 GB.
* NOTE Можно попробовать MAP_HUGETLB, но придётся самостоятельно управлять количеством доступных страниц.
*/
static constexpr size_t MMAP_THRESHOLD = 64 * (1 << 20);
static constexpr size_t HUGE_PAGE_SIZE = 2 * (1 << 20);
public:
/// Выделить кусок памяти и заполнить его нулями.
@ -52,6 +53,14 @@ public:
if (MAP_FAILED == buf)
DB::throwFromErrno("HashTableAllocator: Cannot mmap.", DB::ErrorCodes::CANNOT_ALLOCATE_MEMORY);
/** Использование huge pages позволяет увеличить производительность более чем в три раза
* в запросе SELECT number % 1000000 AS k, count() FROM system.numbers GROUP BY k,
* (хэш-таблица на 1 000 000 элементов)
* и примерно на 15% в случае хэш-таблицы на 100 000 000 элементов.
*/
if (size >= HUGE_PAGE_SIZE && 0 != madvise(buf, size, MADV_HUGEPAGE))
DB::throwFromErrno("HashTableAllocator: Cannot madvise with MADV_HUGEPAGE.", DB::ErrorCodes::CANNOT_ALLOCATE_MEMORY);
/// Заполнение нулями не нужно - mmap сам это делает.
}
else
@ -108,6 +117,10 @@ public:
if (MAP_FAILED == buf)
DB::throwFromErrno("HashTableAllocator: Cannot mremap.", DB::ErrorCodes::CANNOT_MREMAP);
/** Здесь не получается сделать madvise с MADV_HUGEPAGE.
* Похоже, что при mremap, huge pages сами расширяются на новую область.
*/
/// Заполнение нулями не нужно.
}
else

View File

@ -1,7 +1,6 @@
#pragma once
#include <string.h>
#include <malloc.h>
#include <cstddef>
#include <algorithm>
#include <memory>
@ -12,7 +11,7 @@
#include <Yandex/likely.h>
#include <Yandex/strong_typedef.h>
#include <DB/Common/MemoryTracker.h>
#include <DB/Common/Allocator.h>
#include <DB/Core/Exception.h>
#include <DB/Core/ErrorCodes.h>
@ -32,28 +31,18 @@ namespace DB
* Конструктор по-умолчанию создаёт пустой объект, который не выделяет память.
* Затем выделяется память минимум под POD_ARRAY_INITIAL_SIZE элементов.
*
* При первом выделении памяти использует std::allocator.
* В реализации из libstdc++ он кэширует куски памяти несколько больше, чем обычный malloc.
*
* При изменении размера, использует realloc, который может (но не обязан) использовать mremap для больших кусков памяти.
* По факту, mremap используется при использовании аллокатора из glibc, но не используется, например, в tcmalloc.
*
* Если вставлять элементы push_back-ом, не делая reserve, то PODArray примерно в 2.5 раза быстрее std::vector.
*/
#define POD_ARRAY_INITIAL_SIZE 4096UL
template <typename T>
class PODArray : private boost::noncopyable, private std::allocator<char> /// empty base optimization
class PODArray : private boost::noncopyable, private Allocator /// empty base optimization
{
private:
typedef std::allocator<char> Allocator;
char * c_start;
char * c_end;
char * c_end_of_storage;
bool use_libc_realloc = false;
T * t_start() { return reinterpret_cast<T *>(c_start); }
T * t_end() { return reinterpret_cast<T *>(c_end); }
T * t_end_of_storage() { return reinterpret_cast<T *>(c_end_of_storage); }
@ -90,10 +79,7 @@ private:
size_t bytes_to_alloc = to_size(n);
if (current_memory_tracker)
current_memory_tracker->alloc(bytes_to_alloc);
c_start = c_end = Allocator::allocate(bytes_to_alloc);
c_start = c_end = reinterpret_cast<char *>(Allocator::alloc(bytes_to_alloc));
c_end_of_storage = c_start + bytes_to_alloc;
}
@ -102,13 +88,7 @@ private:
if (c_start == nullptr)
return;
if (use_libc_realloc)
::free(c_start);
else
Allocator::deallocate(c_start, storage_size());
if (current_memory_tracker)
current_memory_tracker->free(storage_size());
Allocator::free(c_start, storage_size());
}
void realloc(size_t n)
@ -122,38 +102,10 @@ private:
ptrdiff_t end_diff = c_end - c_start;
size_t bytes_to_alloc = to_size(n);
char * old_c_start = c_start;
char * old_c_end_of_storage = c_end_of_storage;
if (current_memory_tracker)
current_memory_tracker->realloc(storage_size(), bytes_to_alloc);
if (use_libc_realloc)
{
auto new_c_start = reinterpret_cast<char *>(::realloc(c_start, bytes_to_alloc));
if (nullptr == new_c_start)
throwFromErrno("PODArray: cannot realloc", ErrorCodes::CANNOT_ALLOCATE_MEMORY);
c_start = new_c_start;
}
else
{
auto new_c_start = reinterpret_cast<char *>(malloc(bytes_to_alloc));
if (nullptr == new_c_start)
throwFromErrno("PODArray: cannot realloc", ErrorCodes::CANNOT_ALLOCATE_MEMORY);
c_start = new_c_start;
memcpy(c_start, old_c_start, std::min(bytes_to_alloc, static_cast<size_t>(end_diff)));
Allocator::deallocate(old_c_start, old_c_end_of_storage - old_c_start);
}
c_start = reinterpret_cast<char *>(Allocator::realloc(c_start, storage_size(), bytes_to_alloc));
c_end = c_start + end_diff;
c_end_of_storage = c_start + bytes_to_alloc;
use_libc_realloc = true;
}
public:
@ -187,7 +139,6 @@ public:
std::swap(c_start, other.c_start);
std::swap(c_end, other.c_end);
std::swap(c_end_of_storage, other.c_end_of_storage);
std::swap(use_libc_realloc, other.use_libc_realloc);
return *this;
}

View File

@ -3,7 +3,7 @@
#include <boost/noncopyable.hpp>
#include <DB/Common/ProfileEvents.h>
#include <DB/Common/MemoryTracker.h>
#include <DB/Common/Allocator.h>
#include <DB/Core/Exception.h>
#include <DB/Core/ErrorCodes.h>
@ -18,7 +18,7 @@ namespace DB
* Отличается тем, что не делает лишний memset. (И почти ничего не делает.)
* Также можно попросить выделять выровненный кусок памяти.
*/
struct Memory : boost::noncopyable
struct Memory : boost::noncopyable, Allocator
{
size_t m_capacity = 0;
size_t m_size = 0;
@ -66,16 +66,22 @@ struct Memory : boost::noncopyable
}
else
{
dealloc();
new_size = align(new_size);
m_data = reinterpret_cast<char *>(Allocator::realloc(m_data, m_capacity, new_size, alignment));
m_capacity = new_size;
m_size = m_capacity;
alloc();
}
}
private:
size_t align(size_t value) const
{
if (!alignment)
return value;
return (value + alignment - 1) / alignment * alignment;
}
void alloc()
{
if (!m_capacity)
@ -87,33 +93,10 @@ private:
ProfileEvents::increment(ProfileEvents::IOBufferAllocs);
ProfileEvents::increment(ProfileEvents::IOBufferAllocBytes, m_capacity);
if (current_memory_tracker)
current_memory_tracker->alloc(m_capacity);
char * new_m_data = nullptr;
if (!alignment)
{
new_m_data = reinterpret_cast<char *>(malloc(m_capacity));
if (!new_m_data)
throw Exception("Cannot allocate memory (malloc)", ErrorCodes::CANNOT_ALLOCATE_MEMORY);
m_data = new_m_data;
return;
}
size_t aligned_capacity = (m_capacity + alignment - 1) / alignment * alignment;
m_capacity = aligned_capacity;
size_t new_capacity = align(m_capacity);
m_data = reinterpret_cast<char *>(Allocator::alloc(new_capacity, alignment));
m_capacity = new_capacity;
m_size = m_capacity;
int res = posix_memalign(reinterpret_cast<void **>(&new_m_data), alignment, m_capacity);
if (0 != res)
DB::throwFromErrno("Cannot allocate memory (posix_memalign)", ErrorCodes::CANNOT_ALLOCATE_MEMORY, res);
m_data = new_m_data;
}
void dealloc()
@ -121,11 +104,8 @@ private:
if (!m_data)
return;
free(reinterpret_cast<void *>(m_data));
Allocator::free(reinterpret_cast<void *>(m_data), m_capacity);
m_data = nullptr; /// Чтобы избежать double free, если последующий вызов alloc кинет исключение.
if (current_memory_tracker)
current_memory_tracker->free(m_capacity);
}
};