| // This file is part of Eigen, a lightweight C++ template library |
| // for linear algebra. |
| // |
| // This Source Code Form is subject to the terms of the Mozilla |
| // Public License v. 2.0. If a copy of the MPL was not distributed |
| // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| // SPDX-FileCopyrightText: The Eigen Authors |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| #include "main.h" |
| |
| #ifdef EIGEN_CPU_CACHE_SYSFS |
| #include <sched.h> |
| #include <sys/stat.h> |
| #include <cstdlib> |
| #include <string> |
| #include <vector> |
| #endif |
| |
| void cache_sizes_plausible() { |
| // Whatever the platform reports has to be a plausible data-cache size: a stray unit suffix |
| // would otherwise be read as a handful of bytes and silently shrink every blocking size. |
| std::ptrdiff_t l1 = -1, l2 = -1, l3 = -1, l3_per_cpu = -1; |
| internal::queryCacheSizes(l1, l2, l3, l3_per_cpu); |
| for (std::ptrdiff_t size : {l1, l2, l3, l3_per_cpu}) |
| if (size > 0) VERIFY(size >= 1024); |
| // A share is one CPU's slice of one L3 instance, so it can never exceed the reported L3. |
| VERIFY(l3_per_cpu >= 0); |
| if (l3_per_cpu > 0) VERIFY(l3 > 0 && l3_per_cpu <= l3); |
| } |
| |
| #ifdef EIGEN_CPU_CACHE_SYSFS |
| |
| using internal::CpuCacheTopology; |
| |
| bool same_topology(const CpuCacheTopology& a, const CpuCacheTopology& b) { |
| return a.l1 == b.l1 && a.l2 == b.l2 && a.l3 == b.l3 && a.l3_per_cpu == b.l3_per_cpu; |
| } |
| |
| CpuCacheTopology topology_of(std::ptrdiff_t l1, std::ptrdiff_t l2, std::ptrdiff_t l3, std::ptrdiff_t l3_per_cpu) { |
| CpuCacheTopology topology; |
| topology.l1 = l1; |
| topology.l2 = l2; |
| topology.l3 = l3; |
| topology.l3_per_cpu = l3_per_cpu; |
| return topology; |
| } |
| |
| void cache_sizes_parsers() { |
| VERIFY(internal::parseCpuCacheSize("64K\n") == 64 * 1024); |
| VERIFY(internal::parseCpuCacheSize("2048K\n") == 2048 * 1024); |
| VERIFY(internal::parseCpuCacheSize("32M\n") == 32 * 1024 * 1024); |
| // A size Eigen cannot make sense of has to read as "unknown", never as a few bytes. |
| VERIFY(internal::parseCpuCacheSize("bogus") == 0); |
| VERIFY(internal::parseCpuCacheSize("0K") == 0); |
| // A bare count is bytes, not kibibytes. |
| VERIFY(internal::parseCpuCacheSize("512\n") == 512); |
| |
| VERIFY(internal::parseCpuListCount("0\n") == 1); |
| VERIFY(internal::parseCpuListCount("0-3\n") == 4); |
| VERIFY(internal::parseCpuListCount("0-3,8-11\n") == 8); |
| VERIFY(internal::parseCpuListCount("0,2,4\n") == 3); |
| VERIFY(internal::parseCpuListCount("") == 0); |
| VERIFY(internal::parseCpuListCount("3-0") == 0); |
| VERIFY(internal::parseCpuListCount("0-3\r\n") == 4); |
| // Trailing text means the format is not the one assumed, so the count is not trustworthy: |
| // a small count here would inflate l3_per_cpu rather than leave the share unknown. |
| VERIFY(internal::parseCpuListCount("0-3junk") == 0); |
| VERIFY(internal::parseCpuListCount("0-3 8-11") == 0); |
| VERIFY(internal::parseCpuListCount("0-3,") == 0); |
| VERIFY(internal::parseCpuListCount("0-3,junk") == 0); |
| VERIFY(internal::parseCpuListCount("junk") == 0); |
| // strtol would accept a sign, but no CPU has a negative id, and none has one beyond what a kernel can number. |
| VERIFY(internal::parseCpuListCount("-1") == 0); |
| VERIFY(internal::parseCpuListCount("0-70000") == 0); |
| |
| // The ranges are handed over one at a time, and a list that turns out malformed is reported as such even |
| // though its leading ranges were already visited. |
| int ranges = 0, cpus = 0; |
| const auto tally = [&](int first, int last) { |
| ++ranges; |
| cpus += last - first + 1; |
| }; |
| VERIFY(internal::parseCpuList("0-3,8,10-11\n", tally)); |
| VERIFY(ranges == 3 && cpus == 7); |
| VERIFY(!internal::parseCpuList("0-3,8,10-11junk\n", tally)); |
| } |
| |
| // A sysfs tree laid out exactly as Linux publishes it, describing whatever machine a test needs. |
| struct SysfsFixture { |
| std::string root; |
| std::vector<std::string> created; |
| |
| SysfsFixture() { |
| const char* base = std::getenv("TMPDIR"); |
| const std::string pattern = std::string(base != nullptr ? base : "/tmp") + "/eigen-cache-sizes-XXXXXX"; |
| std::vector<char> buffer(pattern.begin(), pattern.end()); |
| buffer.push_back('\0'); |
| if (mkdtemp(buffer.data()) != nullptr) { |
| root = buffer.data(); |
| created.push_back(root); |
| } |
| } |
| ~SysfsFixture() { |
| for (auto it = created.rbegin(); it != created.rend(); ++it) std::remove(it->c_str()); |
| } |
| |
| void directory(const std::string& path) { |
| if (mkdir(path.c_str(), 0700) == 0) created.push_back(path); |
| } |
| void file(const std::string& relative, const char* text) { |
| const std::string path = root + "/" + relative; |
| std::FILE* stream = std::fopen(path.c_str(), "w"); |
| VERIFY(stream != nullptr); |
| std::fputs(text, stream); |
| std::fclose(stream); |
| created.push_back(path); |
| } |
| void cpu(int id) { |
| directory(root + "/cpu" + std::to_string(id)); |
| directory(root + "/cpu" + std::to_string(id) + "/cache"); |
| } |
| void cache(int id, int index, const char* level, const char* type, const char* size, const char* shared) { |
| const std::string dir = "cpu" + std::to_string(id) + "/cache/index" + std::to_string(index); |
| directory(root + "/" + dir); |
| file(dir + "/level", level); |
| file(dir + "/type", type); |
| file(dir + "/size", size); |
| file(dir + "/shared_cpu_list", shared); |
| } |
| }; |
| |
| void cache_sizes_fixture_topology() { |
| SysfsFixture fixture; |
| if (fixture.root.empty()) return; |
| const char* const root = fixture.root.c_str(); |
| |
| // Two big cores (0 and 3) with a 16MB L3 between them, and two little cores (1 and 2) with a 2MB |
| // cluster L3. CPU 3 is online but publishes no cache directory at all, and CPU 2 has no L2, so its |
| // L3 sits at the index the others use for L2. |
| fixture.cpu(0); |
| fixture.cache(0, 0, "1\n", "Data\n", "64K\n", "0\n"); |
| fixture.cache(0, 1, "1\n", "Instruction\n", "128K\n", "0\n"); |
| fixture.cache(0, 2, "2\n", "Unified\n", "1024K\n", "0\n"); |
| fixture.cache(0, 3, "3\n", "Unified\n", "16384K\n", "0,3\n"); |
| fixture.cpu(1); |
| fixture.cache(1, 0, "1\n", "Data\n", "32K\n", "1\n"); |
| fixture.cache(1, 1, "1\n", "Instruction\n", "32K\n", "1\n"); |
| fixture.cache(1, 2, "2\n", "Unified\n", "256K\n", "1\n"); |
| fixture.cache(1, 3, "3\n", "Unified\n", "2048K\n", "1-2\n"); |
| fixture.cpu(2); |
| fixture.cache(2, 0, "1\n", "Data\n", "32K\n", "2\n"); |
| fixture.cache(2, 1, "1\n", "Instruction\n", "32K\n", "2\n"); |
| fixture.cache(2, 2, "3\n", "Unified\n", "2048K\n", "1-2\n"); |
| fixture.directory(fixture.root + "/cpu3"); |
| fixture.file("online", "0-3\n"); |
| |
| const CpuCacheTopology big = topology_of(64 * 1024, 1024 * 1024, 16384 * 1024, 8192 * 1024); |
| const CpuCacheTopology little = topology_of(32 * 1024, 256 * 1024, 2048 * 1024, 1024 * 1024); |
| const CpuCacheTopology unknown = topology_of(0, 0, 0, 0); |
| |
| // Each CPU on its own. The larger instruction cache must not pass for the L1 data cache. |
| VERIFY(same_topology(internal::readCpuCacheTopologySysfs(root, 0), big)); |
| VERIFY(same_topology(internal::readCpuCacheTopologySysfs(root, 1), little)); |
| VERIFY( |
| same_topology(internal::readCpuCacheTopologySysfs(root, 2), topology_of(32 * 1024, 0, 2048 * 1024, 1024 * 1024))); |
| VERIFY(same_topology(internal::readCpuCacheTopologySysfs(root, 3), unknown)); |
| |
| // A thread that may land on any CPU has to block for the smallest caches among them. |
| const auto every_cpu = [](int) { return true; }; |
| VERIFY(same_topology(internal::queryCpuCacheTopologySysfs(root, every_cpu), little)); |
| // One confined to the big cores sees only their geometry, and a mask that excludes CPU 0 must not carry |
| // CPU 0's geometry along. |
| VERIFY(same_topology(internal::queryCpuCacheTopologySysfs(root, [](int cpu) { return cpu == 0; }), big)); |
| VERIFY(same_topology(internal::queryCpuCacheTopologySysfs(root, [](int cpu) { return cpu == 0 || cpu == 3; }), big)); |
| VERIFY(same_topology(internal::queryCpuCacheTopologySysfs(root, [](int cpu) { return cpu != 0; }), little)); |
| VERIFY( |
| same_topology(internal::queryCpuCacheTopologySysfs(root, [](int cpu) { return cpu == 1 || cpu == 3; }), little)); |
| // A CPU that reports nothing, or no CPU at all, leaves the geometry unknown rather than defaulted. |
| VERIFY(same_topology(internal::queryCpuCacheTopologySysfs(root, [](int cpu) { return cpu == 3; }), unknown)); |
| VERIFY(same_topology(internal::queryCpuCacheTopologySysfs(root, [](int) { return false; }), unknown)); |
| |
| // An online list that cannot be trusted, or that is missing, means no CPU is known. |
| fixture.file("online", "0-3junk\n"); |
| VERIFY(same_topology(internal::queryCpuCacheTopologySysfs(root, every_cpu), unknown)); |
| fixture.file("online", "0-3"); |
| VERIFY(same_topology(internal::queryCpuCacheTopologySysfs(root, every_cpu), unknown)); |
| std::remove((fixture.root + "/online").c_str()); |
| VERIFY(same_topology(internal::queryCpuCacheTopologySysfs(root, every_cpu), unknown)); |
| } |
| |
| void cache_sizes_affinity() { |
| // On a kernel that publishes the topology, Eigen has to pick it up instead of falling back to |
| // its compiled-in defaults. glibc's sysconf answers only on x86, so before the sysfs fallback |
| // this failed on every other Linux architecture. |
| std::ptrdiff_t l1, l2, l3, l3_per_cpu; |
| internal::queryCacheSizes(l1, l2, l3, l3_per_cpu); |
| const CpuCacheTopology mine = internal::queryCpuCacheTopologySysfs(); |
| if (mine.l1 > 0) VERIFY(l1 > 0); |
| VERIFY(l3_per_cpu == mine.l3_per_cpu); |
| |
| // Confine the thread to every CPU it may use but CPU 0: the detected geometry has to follow the mask. |
| cpu_set_t original; |
| if (sched_getaffinity(0, sizeof(original), &original) != 0) return; |
| if (CPU_COUNT(&original) < 2 || !CPU_ISSET(0, &original)) return; |
| cpu_set_t restricted = original; |
| CPU_CLR(0, &restricted); |
| VERIFY(sched_setaffinity(0, sizeof(restricted), &restricted) == 0); |
| const CpuCacheTopology seen = internal::queryCpuCacheTopologySysfs(); |
| const CpuCacheTopology expected = internal::queryCpuCacheTopologySysfs( |
| "/sys/devices/system/cpu", [&restricted](int cpu) { return cpu < CPU_SETSIZE && CPU_ISSET(cpu, &restricted); }); |
| VERIFY(sched_setaffinity(0, sizeof(original), &original) == 0); |
| VERIFY(same_topology(seen, expected)); |
| } |
| |
| #endif // EIGEN_CPU_CACHE_SYSFS |
| |
| EIGEN_DECLARE_TEST(cache_sizes) { |
| CALL_SUBTEST(cache_sizes_plausible()); |
| #ifdef EIGEN_CPU_CACHE_SYSFS |
| CALL_SUBTEST(cache_sizes_parsers()); |
| CALL_SUBTEST(cache_sizes_fixture_topology()); |
| CALL_SUBTEST(cache_sizes_affinity()); |
| #endif |
| } |