Allocation Optimizations#29
Conversation
…im it, persist the buffer and copy a portion of it when needed
…ore reallocations
| const std::string& prefix, | ||
| const uint64_t batchsize) noexcept | ||
| : m_prefix(prefix), m_sender(new UDPSender{host, port, batchsize}) { | ||
| : m_prefix(prefix), m_sender(new UDPSender{host, port, batchsize}), m_buffer(256, '\0') { |
There was a problem hiding this comment.
256 seems big enough to me but maybe sometime in the future we could make it configurable
| } | ||
|
|
||
| // Prepare the buffer and include the sampling rate if it's not 1.f | ||
| std::string buffer(256, '\0'); |
There was a problem hiding this comment.
now we dont have to make a new string every time we send
| } else { | ||
| m_batchingMessageQueue.rbegin()->append("\n").append(message); | ||
| m_batchingMessageQueue.emplace_back(); | ||
| m_batchingMessageQueue.back().reserve(m_batchsize + 256); |
There was a problem hiding this comment.
when we need a new batch started, we now reserve enough bytes that we will not need to reallocate it again.
There was a problem hiding this comment.
We could make this function slightly clearer with something like
inline void UDPSender::send(const std::string& message) noexcept {
m_errorMessage.clear();
// If batching is on, accumulate messages in the queue
if (m_batching) {
queueMessage(message);
return;
}
sendToDaemon(message);
}
inline void UDPSender::queueMessage(const std::string& message) noexcept {
std::unique_lock<std::mutex> batchingLock(m_batchingMutex);
// Either we don't have a place to batch our message or we exceeded the batch size, so make a new batch
if (m_batchingMessageQueue.empty() || m_batchingMessageQueue.back().length() > m_batchsize) {
m_batchingMessageQueue.emplace_back();
m_batchingMessageQueue.back().reserve(m_batchsize + 256);
} // When there is already a batch open we need a separator when its not empty
else if (!m_batchingMessageQueue.back().empty()) {
m_batchingMessageQueue.back().push_back('\n');
}
// Add the new message to the batch
m_batchingMessageQueue.back().append(message);
}| close(m_socket); | ||
| m_socket = k_invalidFd; | ||
| m_errorMessage = "getaddrinfo failed: error=" + std::to_string(ret) + ", msg=" + gai_strerror(ret); | ||
| m_errorMessage = "getaddrinfo failed: err=" + std::to_string(ret) + ", msg=" + gai_strerror(ret); |
There was a problem hiding this comment.
i tried to make all the error message look as much like each other as possible. its still not perfect but slightly better i guess
| if (!client.errorMessage().empty()) { | ||
| std::cerr << client.errorMessage() << std::endl; | ||
| throw std::runtime_error(client.errorMessage()); | ||
| void throwOnError(const StatsdClient& client, bool expectEmpty = true, const std::string& extraMessage = "") { |
There was a problem hiding this comment.
i realized i wanted to test when i expected there not to be an error AND when i expected there to be an error. so i extended this helper to allow for testing both possiblities
| std::cerr << "Should not be able to connect to ridiculous ip" << std::endl; | ||
| throw std::runtime_error("Should not be able to connect to ridiculous ip"); | ||
| } | ||
| throwOnError(client, false, "Should not be able to resolve a ridiculous ip"); |
There was a problem hiding this comment.
so now testing for when we expect an error is way less verbose.
|
Thanks for the work and all the comments! I'll review it asap |
|
@vthiery in the mean time i will get the conflict resolved! thank you so much! |
|
@vthiery the conflicts should be resolved now, let me know what you think! |
|
@vthiery is there anything i should do here to make this easier for review or are you just trying to find the time for it? let me know if there is anything else i should do here! |
Sorry for the delay. There is nothing left to do on your side, I just need to find some time to review it properly. I'll try to do it tomorrow. |
|
Well I started implementing the other PR for protecting against sending the wrong stats types and I was adding support for tags and I started to question what would be the best way to support tags. This lead me down a rabbit hole of what is the best way to format a string. I was pretty shocked at the results, but checkout this gist: https://gist.github.com/kevinkreiser/1a7c9a7cc803a51e79f76a5e0e53730f#file-string_format-cpp-L17-L20 Based on my testing it seems using string operations directly is faster than printf-like functions, which I think makes decent sense. So I'm going to refactor this PR to do that as well. |
| value, | ||
| type.c_str(), | ||
| frequency); | ||
| // Format the stat message |
There was a problem hiding this comment.
it was faster to use string operations than snprintf, i benchmarked it here: https://gist.github.com/kevinkreiser/1a7c9a7cc803a51e79f76a5e0e53730f#file-string_format-cpp
the other benefit is that now we dont have to worry if the string we are format is larger than the buffer we have. when for some reason there is a really large stat message, the std::string will allocate more space to fit it. in practice this should be very unlikely.
|
Ok cool I've updated the code to use string operations which simplifies this PR, makes the code faster, and makes the code easier to update in my next PR 😄 |
vthiery
left a comment
There was a problem hiding this comment.
Looks good! Only a couple of minor details to fix
| } else { | ||
| m_batchingMessageQueue.rbegin()->append("\n").append(message); | ||
| m_batchingMessageQueue.emplace_back(); | ||
| m_batchingMessageQueue.back().reserve(m_batchsize + 256); |
There was a problem hiding this comment.
We could make this function slightly clearer with something like
inline void UDPSender::send(const std::string& message) noexcept {
m_errorMessage.clear();
// If batching is on, accumulate messages in the queue
if (m_batching) {
queueMessage(message);
return;
}
sendToDaemon(message);
}
inline void UDPSender::queueMessage(const std::string& message) noexcept {
std::unique_lock<std::mutex> batchingLock(m_batchingMutex);
// Either we don't have a place to batch our message or we exceeded the batch size, so make a new batch
if (m_batchingMessageQueue.empty() || m_batchingMessageQueue.back().length() > m_batchsize) {
m_batchingMessageQueue.emplace_back();
m_batchingMessageQueue.back().reserve(m_batchsize + 256);
} // When there is already a batch open we need a separator when its not empty
else if (!m_batchingMessageQueue.back().empty()) {
m_batchingMessageQueue.back().push_back('\n');
}
// Add the new message to the batch
m_batchingMessageQueue.back().append(message);
}Co-authored-by: Vincent Thiery <vjmthiery@gmail.com>
|
ok lint should be good now, there were spaces in the extra lines between the string formating code blocks. thanks again @vthiery! |
|
Thanks for the contribution @kevinkreiser ! |
In a previous PR I noticed that we instantiated a
char[]only to copy it into a string and then potentially copy that string further on down the chain. In that previous PR I changed thechar[]to a fixed size string but still the string was being allocated on ever call tosendand again potentially copied.This PR moves that buffer to a member variable, to which we write our messages and from which we read to send bytes over the socket. When in batching mode I've also made some changes to remove as many re-allocations as possible (by reserving the batch size for any new batches that are made). I've changed the send interface of the
UDPSenderto use iterators so that we don't copy any bytes from theStatsdClientto theUDPSender.One strange quirk that I noticed a while back that we dont handle is when a person tries to send a stat that is larger than our buffer.
snprintftells us when this happens but we previously didnt do anything about it. i've added a unit test for that case but annoyingly since theUDPSenderowns the error message it was somewhat un-tidy to allow theStatsdClientto set this message.