Loading...
Searching...
No Matches
memory.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * Resident memory sampling for Q3.
5 *
6 * Answers "how much memory does this rank hold right now, and what is the most
7 * it has ever held", so that a footprint claim can be measured rather than
8 * derived from array sizes. The peak matters more than the current value: the
9 * Davidson basis grows across iterations and the collapse workspaces appear
10 * only at the first restart, so any single early reading understates the run.
11 *
12 * Where the platform offers it, the sample also splits resident memory into
13 * anonymous and shared pages and reports what the allocator believes it is
14 * holding. That split is what distinguishes memory Q3 allocated from memory an
15 * MPI transport mapped underneath it.
16 */
17
18#include <cstdint>
19#include <cstdio>
20#include <string>
21
22#if defined(__linux__)
23#include <cstring>
24#include <sys/resource.h>
25#if defined(__GLIBC__)
26#include <malloc.h>
27#define Q3_HAS_MALLOC_TRIM 1
28#if defined(__GLIBC_PREREQ) && __GLIBC_PREREQ(2, 33)
29#define Q3_HAS_MALLINFO2 1
30#endif
31#endif
32#elif defined(__APPLE__)
33#include <mach/mach.h>
34#include <malloc/malloc.h>
35#include <sys/resource.h>
36#endif
37
38namespace q3 {
39
40/**
41 * One reading of this process's resident memory, in bytes.
42 *
43 * `available` is false when no source could be read; every field is then zero.
44 * Sampling never throws and never fails the run — a missing reading is worth
45 * less than a crashed calculation.
46 */
48 /// Resident set size at the moment of sampling.
49 std::uint64_t current_bytes = 0;
50 /// High-water mark of the resident set over the life of the process.
51 std::uint64_t peak_bytes = 0;
52 /// Resident anonymous pages: heap and stack, what this process allocated.
53 std::uint64_t anonymous_bytes = 0;
54 /// Resident shared pages: mapped files and shared-memory segments.
55 std::uint64_t shared_bytes = 0;
56 /// Bytes the C allocator reports as handed out, where it can be asked.
57 std::uint64_t malloc_in_use_bytes = 0;
58 /// Whether any source could be read.
59 bool available = false;
60
62
63 static double to_gibibytes(std::uint64_t bytes) {
64 return static_cast<double>(bytes) / 1073741824.0;
65 }
66
67 static std::string format_gibibytes(std::uint64_t bytes);
68};
69
70#if defined(__linux__)
71
72/**
73 * Linux reads /proc/self/status, the only source that separates anonymous from
74 * shared pages and the only one that reports a peak without a syscall per
75 * field. getrusage supplies the peak if the file cannot be opened.
76 */
77inline ResidentMemorySample ResidentMemorySample::sample() {
78 ResidentMemorySample result;
79 std::FILE* status_file = std::fopen("/proc/self/status", "r");
80 if (status_file != nullptr) {
81 char line[256];
82 while (std::fgets(line, sizeof(line), status_file) != nullptr) {
83 unsigned long long kilobytes = 0;
84 if (std::sscanf(line, "VmRSS: %llu kB", &kilobytes) == 1) {
85 result.current_bytes = static_cast<std::uint64_t>(kilobytes) * 1024;
86 result.available = true;
87 } else if (std::sscanf(line, "VmHWM: %llu kB", &kilobytes) == 1) {
88 result.peak_bytes = static_cast<std::uint64_t>(kilobytes) * 1024;
89 result.available = true;
90 } else if (std::sscanf(line, "RssAnon: %llu kB", &kilobytes) == 1) {
91 result.anonymous_bytes = static_cast<std::uint64_t>(kilobytes) * 1024;
92 } else if (std::sscanf(line, "RssShmem: %llu kB", &kilobytes) == 1) {
93 result.shared_bytes = static_cast<std::uint64_t>(kilobytes) * 1024;
94 }
95 }
96 std::fclose(status_file);
97 }
98 if (result.peak_bytes == 0) {
99 struct rusage usage;
100 if (getrusage(RUSAGE_SELF, &usage) == 0) {
101 result.peak_bytes = static_cast<std::uint64_t>(usage.ru_maxrss) * 1024;
102 result.available = true;
103 }
104 }
105#if defined(Q3_HAS_MALLINFO2)
106 struct mallinfo2 allocator_info = mallinfo2();
107 result.malloc_in_use_bytes = static_cast<std::uint64_t>(allocator_info.uordblks) +
108 static_cast<std::uint64_t>(allocator_info.hblkhd);
109#endif
110 return result;
111}
112
113#elif defined(__APPLE__)
114
115/**
116 * macOS has no /proc, so the current size comes from the Mach task info and the
117 * peak from whichever of the task info and getrusage is larger. ru_maxrss is in
118 * bytes here, unlike Linux. No anonymous/shared split is reported.
119 */
120inline ResidentMemorySample ResidentMemorySample::sample() {
121 ResidentMemorySample result;
122 mach_task_basic_info task_information;
123 mach_msg_type_number_t information_count = MACH_TASK_BASIC_INFO_COUNT;
124 if (task_info(mach_task_self(),
125 MACH_TASK_BASIC_INFO,
126 reinterpret_cast<task_info_t>(&task_information),
127 &information_count) == KERN_SUCCESS) {
128 result.current_bytes = static_cast<std::uint64_t>(task_information.resident_size);
129 result.peak_bytes = static_cast<std::uint64_t>(task_information.resident_size_max);
130 result.available = true;
131 }
132 struct rusage usage;
133 if (getrusage(RUSAGE_SELF, &usage) == 0) {
134 std::uint64_t reported_peak = static_cast<std::uint64_t>(usage.ru_maxrss);
135 if (reported_peak > result.peak_bytes) {
136 result.peak_bytes = reported_peak;
137 }
138 result.available = true;
139 }
140 return result;
141}
142
143#else
144
145/// Platforms without a known source report nothing rather than guessing.
149
150#endif
151
152inline std::string ResidentMemorySample::format_gibibytes(std::uint64_t bytes) {
153 char buffer[32];
154 std::snprintf(buffer, sizeof(buffer), "%.2f GiB", to_gibibytes(bytes));
155 return std::string(buffer);
156}
157
158/**
159 * Hand freed heap back to the operating system.
160 *
161 * Both allocators keep the pages they were given after every block in them
162 * has been freed, and the resident set -- what a scheduler or an OOM killer
163 * counts -- keeps them too. At four ranks on the 54-atom silicon cell a rank
164 * sat at 0.85 GiB with 0.32 GiB of heap in use: the rest was the setup
165 * transients and the solver's per-iteration temporaries, dead but resident.
166 * glibc's malloc_trim and macOS's pressure relief return what they can, and
167 * this is called at phase boundaries. Measured, it does not lower the peak
168 * inside an SCF iteration -- the next iteration's temporaries reuse the
169 * pages it would have returned -- but it does lower what a later phase
170 * starts from: the one-rank run of that cell, whose stress used to stack on
171 * the retained pages, peaks at 4.12 GiB instead of 4.83. Returns the bytes
172 * released where the platform reports them, and 0 where it does not.
173 */
174inline std::uint64_t release_unused_heap() {
175#if defined(Q3_HAS_MALLOC_TRIM)
176 malloc_trim(0);
177 return 0;
178#elif defined(__APPLE__)
179 return static_cast<std::uint64_t>(malloc_zone_pressure_relief(nullptr, 0));
180#else
181 return 0;
182#endif
183}
184
185} // namespace q3
std::uint64_t release_unused_heap()
Definition memory.hpp:174
std::uint64_t anonymous_bytes
Resident anonymous pages: heap and stack, what this process allocated.
Definition memory.hpp:53
static ResidentMemorySample sample()
Platforms without a known source report nothing rather than guessing.
Definition memory.hpp:146
static double to_gibibytes(std::uint64_t bytes)
Definition memory.hpp:63
static std::string format_gibibytes(std::uint64_t bytes)
Definition memory.hpp:152
std::uint64_t malloc_in_use_bytes
Bytes the C allocator reports as handed out, where it can be asked.
Definition memory.hpp:57
std::uint64_t shared_bytes
Resident shared pages: mapped files and shared-memory segments.
Definition memory.hpp:55
std::uint64_t current_bytes
Resident set size at the moment of sampling.
Definition memory.hpp:49
std::uint64_t peak_bytes
High-water mark of the resident set over the life of the process.
Definition memory.hpp:51
bool available
Whether any source could be read.
Definition memory.hpp:59