blob: 18803258f5006744f25da6368edde023a584c2b5 [file] [edit]
// 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
}