gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/test/perf/linux/fork_benchmark.cc (about) 1 // Copyright 2020 The gVisor Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 #include <unistd.h> 16 17 #include "gtest/gtest.h" 18 #include "absl/synchronization/barrier.h" 19 #include "benchmark/benchmark.h" 20 #include "test/util/cleanup.h" 21 #include "test/util/file_descriptor.h" 22 #include "test/util/logging.h" 23 #include "test/util/test_util.h" 24 #include "test/util/thread_util.h" 25 26 namespace gvisor { 27 namespace testing { 28 29 namespace { 30 31 constexpr int kBusyMax = 250; 32 33 // Do some CPU-bound busy-work. 34 int busy(int max) { 35 // Prevent the compiler from optimizing this work away, 36 volatile int count = 0; 37 38 for (int i = 1; i < max; i++) { 39 for (int j = 2; j < i / 2; j++) { 40 if (i % j == 0) { 41 count++; 42 } 43 } 44 } 45 46 return count; 47 } 48 49 void BM_CPUBoundUniprocess(benchmark::State& state) { 50 for (auto _ : state) { 51 busy(kBusyMax); 52 } 53 } 54 55 BENCHMARK(BM_CPUBoundUniprocess); 56 57 void BM_CPUBoundAsymmetric(benchmark::State& state) { 58 const size_t max = state.max_iterations; 59 pid_t child = fork(); 60 if (child == 0) { 61 for (size_t i = 0; i < max; i++) { 62 busy(kBusyMax); 63 } 64 _exit(0); 65 } 66 ASSERT_THAT(child, SyscallSucceeds()); 67 ASSERT_TRUE(state.KeepRunningBatch(max)); 68 69 int status; 70 EXPECT_THAT(RetryEINTR(waitpid)(child, &status, 0), SyscallSucceeds()); 71 EXPECT_TRUE(WIFEXITED(status)); 72 EXPECT_EQ(0, WEXITSTATUS(status)); 73 ASSERT_FALSE(state.KeepRunning()); 74 } 75 76 BENCHMARK(BM_CPUBoundAsymmetric)->UseRealTime(); 77 78 void BM_CPUBoundSymmetric(benchmark::State& state) { 79 std::vector<pid_t> children; 80 auto child_cleanup = Cleanup([&] { 81 for (const pid_t child : children) { 82 int status; 83 EXPECT_THAT(RetryEINTR(waitpid)(child, &status, 0), SyscallSucceeds()); 84 EXPECT_TRUE(WIFEXITED(status)); 85 EXPECT_EQ(0, WEXITSTATUS(status)); 86 } 87 ASSERT_FALSE(state.KeepRunning()); 88 }); 89 90 const int processes = state.range(0); 91 for (int i = 0; i < processes; i++) { 92 size_t cur = (state.max_iterations + (processes - 1)) / processes; 93 if ((state.iterations() + cur) >= state.max_iterations) { 94 cur = state.max_iterations - state.iterations(); 95 } 96 pid_t child = fork(); 97 if (child == 0) { 98 for (size_t i = 0; i < cur; i++) { 99 busy(kBusyMax); 100 } 101 _exit(0); 102 } 103 ASSERT_THAT(child, SyscallSucceeds()); 104 if (cur > 0) { 105 // We can have a zero cur here, depending. 106 ASSERT_TRUE(state.KeepRunningBatch(cur)); 107 } 108 children.push_back(child); 109 } 110 } 111 112 BENCHMARK(BM_CPUBoundSymmetric)->Range(2, 16)->UseRealTime(); 113 114 // Child routine for ProcessSwitch/ThreadSwitch. 115 // Reads from readfd and writes the result to writefd. 116 void SwitchChild(int readfd, int writefd) { 117 while (1) { 118 char buf; 119 int ret = ReadFd(readfd, &buf, 1); 120 if (ret == 0) { 121 break; 122 } 123 TEST_CHECK_MSG(ret == 1, "read failed"); 124 125 ret = WriteFd(writefd, &buf, 1); 126 if (ret == -1) { 127 TEST_CHECK_MSG(errno == EPIPE, "unexpected write failure"); 128 break; 129 } 130 TEST_CHECK_MSG(ret == 1, "write failed"); 131 } 132 } 133 134 // Send bytes in a loop through a series of pipes, each passing through a 135 // different process. 136 // 137 // Proc 0 Proc 1 138 // * ----------> * 139 // ^ Pipe 1 | 140 // | | 141 // | Pipe 0 | Pipe 2 142 // | | 143 // | | 144 // | Pipe 3 v 145 // * <---------- * 146 // Proc 3 Proc 2 147 // 148 // This exercises context switching through multiple processes. 149 void BM_ProcessSwitch(benchmark::State& state) { 150 // Code below assumes there are at least two processes. 151 const int num_processes = state.range(0); 152 ASSERT_GE(num_processes, 2); 153 154 std::vector<pid_t> children; 155 auto child_cleanup = Cleanup([&] { 156 for (const pid_t child : children) { 157 int status; 158 EXPECT_THAT(RetryEINTR(waitpid)(child, &status, 0), SyscallSucceeds()); 159 EXPECT_TRUE(WIFEXITED(status)); 160 EXPECT_EQ(0, WEXITSTATUS(status)); 161 } 162 }); 163 164 // Must come after children, as the FDs must be closed before the children 165 // will exit. 166 std::vector<FileDescriptor> read_fds; 167 std::vector<FileDescriptor> write_fds; 168 169 for (int i = 0; i < num_processes; i++) { 170 int fds[2]; 171 ASSERT_THAT(pipe(fds), SyscallSucceeds()); 172 read_fds.emplace_back(fds[0]); 173 write_fds.emplace_back(fds[1]); 174 } 175 176 // This process is one of the processes in the loop. It will be considered 177 // index 0. 178 for (int i = 1; i < num_processes; i++) { 179 // Read from current pipe index, write to next. 180 const int read_index = i; 181 const int read_fd = read_fds[read_index].get(); 182 183 const int write_index = (i + 1) % num_processes; 184 const int write_fd = write_fds[write_index].get(); 185 186 // std::vector isn't safe to use from the fork child. 187 FileDescriptor* read_array = read_fds.data(); 188 FileDescriptor* write_array = write_fds.data(); 189 190 pid_t child = fork(); 191 if (!child) { 192 // Close all other FDs. 193 for (int j = 0; j < num_processes; j++) { 194 if (j != read_index) { 195 read_array[j].reset(); 196 } 197 if (j != write_index) { 198 write_array[j].reset(); 199 } 200 } 201 202 SwitchChild(read_fd, write_fd); 203 _exit(0); 204 } 205 ASSERT_THAT(child, SyscallSucceeds()); 206 children.push_back(child); 207 } 208 209 // Read from current pipe index (0), write to next (1). 210 const int read_index = 0; 211 const int read_fd = read_fds[read_index].get(); 212 213 const int write_index = 1; 214 const int write_fd = write_fds[write_index].get(); 215 216 // Kick start the loop. 217 char buf = 'a'; 218 ASSERT_THAT(WriteFd(write_fd, &buf, 1), SyscallSucceedsWithValue(1)); 219 220 for (auto _ : state) { 221 ASSERT_THAT(ReadFd(read_fd, &buf, 1), SyscallSucceedsWithValue(1)); 222 ASSERT_THAT(WriteFd(write_fd, &buf, 1), SyscallSucceedsWithValue(1)); 223 } 224 } 225 226 BENCHMARK(BM_ProcessSwitch)->Range(2, 16)->UseRealTime(); 227 228 // Equivalent to BM_ThreadSwitch using threads instead of processes. 229 void BM_ThreadSwitch(benchmark::State& state) { 230 // Code below assumes there are at least two threads. 231 const int num_threads = state.range(0); 232 ASSERT_GE(num_threads, 2); 233 234 // Must come after threads, as the FDs must be closed before the children 235 // will exit. 236 std::vector<std::unique_ptr<ScopedThread>> threads; 237 std::vector<FileDescriptor> read_fds; 238 std::vector<FileDescriptor> write_fds; 239 240 for (int i = 0; i < num_threads; i++) { 241 int fds[2]; 242 ASSERT_THAT(pipe(fds), SyscallSucceeds()); 243 read_fds.emplace_back(fds[0]); 244 write_fds.emplace_back(fds[1]); 245 } 246 247 // This thread is one of the threads in the loop. It will be considered 248 // index 0. 249 for (int i = 1; i < num_threads; i++) { 250 // Read from current pipe index, write to next. 251 // 252 // Transfer ownership of the FDs to the thread. 253 const int read_index = i; 254 const int read_fd = read_fds[read_index].release(); 255 256 const int write_index = (i + 1) % num_threads; 257 const int write_fd = write_fds[write_index].release(); 258 259 threads.emplace_back(std::make_unique<ScopedThread>([read_fd, write_fd] { 260 FileDescriptor read(read_fd); 261 FileDescriptor write(write_fd); 262 SwitchChild(read.get(), write.get()); 263 })); 264 } 265 266 // Read from current pipe index (0), write to next (1). 267 const int read_index = 0; 268 const int read_fd = read_fds[read_index].get(); 269 270 const int write_index = 1; 271 const int write_fd = write_fds[write_index].get(); 272 273 // Kick start the loop. 274 char buf = 'a'; 275 ASSERT_THAT(WriteFd(write_fd, &buf, 1), SyscallSucceedsWithValue(1)); 276 277 for (auto _ : state) { 278 ASSERT_THAT(ReadFd(read_fd, &buf, 1), SyscallSucceedsWithValue(1)); 279 ASSERT_THAT(WriteFd(write_fd, &buf, 1), SyscallSucceedsWithValue(1)); 280 } 281 282 // The two FDs still owned by this thread are closed, causing the next thread 283 // to exit its loop and close its FDs, and so on until all threads exit. 284 } 285 286 BENCHMARK(BM_ThreadSwitch)->Range(2, 16)->UseRealTime(); 287 288 void BM_ThreadStart(benchmark::State& state) { 289 const int num_threads = state.range(0); 290 291 for (auto _ : state) { 292 state.PauseTiming(); 293 294 auto barrier = new absl::Barrier(num_threads + 1); 295 std::vector<std::unique_ptr<ScopedThread>> threads; 296 297 state.ResumeTiming(); 298 299 for (int i = 0; i < num_threads; ++i) { 300 threads.emplace_back(std::make_unique<ScopedThread>([barrier] { 301 if (barrier->Block()) { 302 delete barrier; 303 } 304 })); 305 } 306 307 if (barrier->Block()) { 308 delete barrier; 309 } 310 311 state.PauseTiming(); 312 313 for (const auto& thread : threads) { 314 thread->Join(); 315 } 316 317 state.ResumeTiming(); 318 } 319 } 320 321 BENCHMARK(BM_ThreadStart)->Range(1, 2048)->UseRealTime(); 322 323 // Benchmark the complete fork + exit + wait. 324 void BM_ProcessLifecycle(benchmark::State& state) { 325 const int num_procs = state.range(0); 326 327 std::vector<pid_t> pids(num_procs); 328 for (auto _ : state) { 329 for (int i = 0; i < num_procs; ++i) { 330 int pid = fork(); 331 if (pid == 0) { 332 _exit(0); 333 } 334 ASSERT_THAT(pid, SyscallSucceeds()); 335 pids[i] = pid; 336 } 337 338 for (const int pid : pids) { 339 ASSERT_THAT(RetryEINTR(waitpid)(pid, nullptr, 0), 340 SyscallSucceedsWithValue(pid)); 341 } 342 } 343 } 344 345 BENCHMARK(BM_ProcessLifecycle)->Range(1, 512)->UseRealTime(); 346 347 } // namespace 348 349 } // namespace testing 350 } // namespace gvisor