* [PATCH v8 0/3] exfat: speed up file creation in large directories
@ 2026-10-14 15:31 Yang Wen
2026-10-14 15:31 ` [PATCH v8 1/3] exfat: add a Bloom filter for negative name lookups Yang Wen
` (2 more replies)
0 siblings, 3 replies; 4+ messages in thread
From: Yang Wen @ 2026-10-14 15:31 UTC (permalink / raw)
To: linkinjeon, sj1557.seo, chizhiling
Cc: yuezhang.mo, exfat, linux-kernel, Yang Wen
Negative name lookups and empty-entry allocation can repeatedly scan a
directory from the beginning. Bulk creation in a large directory therefore
approaches O(N^2).
This series separates the optimization into three independently reviewable
steps. Patch 1 adds the Bloom filter used to reject definite name misses.
Patch 2 retains and correctly invalidates the next-empty-entry hint.
Patch 3 adds the LRU and shrinker used to reclaim filters under memory
pressure.
Test environment:
QEMU TCG multi-thread, 4 vCPUs, 6 GiB RAM
4 GiB exFAT image, 32 KiB clusters
Previous baseline Patched v8
real 589.48 s 30.27 s
user 4.72 s 3.40 s
sys 584.63 s 26.83 s
The unpatched baseline is a previously recorded result and was not rerun.
The patched run created 20,000 files; fsck.exfat reported a clean image.
Changes in v8:
- Rebase the series onto exfat/dev at 8e431c3bce0b.
- Drop unrelated blank-line changes from the Bloom-filter patch.
- Discard empty-entry hints collected by positive name lookups, since a
lookup can start at hint_stat and return before scanning the directory
prefix.
- Track the remaining root-directory chain length during volume-label scans
so their empty-entry hints remain valid across cluster boundaries.
- Remove a reclaimed filter from the LRU before clearing the inode pointer,
so the lockless free fast path cannot observe a linked but pointerless
filter during teardown.
Changes in v7:
- Size each Bloom filter from the allocated directory size, using four bits
per on-disk entry and clamping the bitmap between 512 bytes and 4 MiB.
- Track set bits and discard a filter when one third of its bits are set or
directory growth requires a larger bitmap. Rebuild it lazily after a
later complete negative lookup.
- Account reclaimable filters in pages so shrinker accounting follows their
dynamic allocation sizes.
- Preserve the shrinker core's scan budget and return SHRINK_STOP when the
LRU is already empty, preventing repeated no-progress scans.
Changes in v6:
- Invalidate the name filter when committing a new entry set fails, before
attempting rollback. Despite the reported write error, the new name may
already have reached disk.
- Move rename/move error invalidation into the Bloom filter patch so that
each patch remains correct when applied independently.
Changes in v5:
- Rebase the series onto the exFAT maintainer's dev branch.
- Treat every non-negative exfat_find_empty_entry() return value as a
successful allocation in the volume-label path.
- Record the minimum entry-set size for which a saved empty-entry hint is
valid. A shorter entry set now rescans from the beginning and can reuse a
smaller hole that an earlier, longer entry set could not use.
Changes in v4:
- Publish the next-empty-entry hint only after the directory entry set is
successfully committed, so post-allocation failures cannot skip an unused
slot.
- Invalidate the destination name filter when rename or move fails because
the new entry may already exist on disk.
Changes in v3:
- Split the change into Bloom filter, empty-entry hint, and shrinker
patches.
- Accept filenames containing exactly 255 UTF-16 code units while building
the Bloom filter.
- Invalidate the empty-entry hint in every path that can free directory
entries, preventing stale hints from skipping earlier holes.
Changes in v2:
- Move exfat_name_filter_free() to exfat_evict_inode() because
->free_inode() may run from an RCU callback in softirq context.
Yang Wen (3):
exfat: add a Bloom filter for negative name lookups
exfat: retain the next empty directory entry hint
exfat: reclaim name filters under memory pressure
fs/exfat/dir.c | 363 +++++++++++++++++++++++++++++++++++++++++++-
fs/exfat/exfat_fs.h | 30 +++-
fs/exfat/inode.c | 1 +
fs/exfat/namei.c | 87 ++++++++++-
fs/exfat/super.c | 9 ++
5 files changed, 478 insertions(+), 12 deletions(-)
base-commit: 8e431c3bce0bb51802fc79b10225cc90a15f15d1
--
2.34.1
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v8 1/3] exfat: add a Bloom filter for negative name lookups
2026-10-14 15:31 [PATCH v8 0/3] exfat: speed up file creation in large directories Yang Wen
@ 2026-10-14 15:31 ` Yang Wen
2026-10-14 15:31 ` [PATCH v8 2/3] exfat: retain the next empty directory entry hint Yang Wen
2026-10-14 15:31 ` [PATCH v8 3/3] exfat: reclaim name filters under memory pressure Yang Wen
2 siblings, 0 replies; 4+ messages in thread
From: Yang Wen @ 2026-10-14 15:31 UTC (permalink / raw)
To: linkinjeon, sj1557.seo, chizhiling
Cc: yuezhang.mo, exfat, linux-kernel, Yang Wen
Negative name lookups scan a directory from the beginning. Repeating this
scan before creating each file makes bulk creation approach O(N^2).
Add a per-directory Bloom filter and build it lazily after a directory
reaches 1024 on-disk entries. Size its bitmap at four bits per on-disk
entry, rounded up to a power of two and clamped between 512 bytes and
4 MiB. A definite miss skips the directory scan, while a possible
match follows the normal lookup path, so hash collisions cannot affect
correctness.
A short name needs at least three directory entries, so four bits per entry
provide at least 12 bits per name. A 512-byte bitmap covers about 340 short
names with a 1.07% false-positive rate, while one million short names use a
2 MiB bitmap with a 0.44% rate. The maximum directory uses at most a 4 MiB
bitmap.
Track the number of set bits exactly. Drop a filter when one third of its
bits are set, where the estimated false-positive rate is 3.70%, or when
directory growth requires a larger filter. Rebuild it after a later
complete negative lookup. Also invalidate the filter when committing a new
entry set fails, or when writing a renamed or moved entry reports an error,
because an entry can reach disk before failure is reported.
Signed-off-by: Yang Wen <anmuxixixi@gmail.com>
---
fs/exfat/dir.c | 185 ++++++++++++++++++++++++++++++++++++++++++++
fs/exfat/exfat_fs.h | 15 ++++
fs/exfat/inode.c | 1 +
fs/exfat/namei.c | 6 ++
fs/exfat/super.c | 1 +
5 files changed, 208 insertions(+)
diff --git a/fs/exfat/dir.c b/fs/exfat/dir.c
index 46514b13bebd..dcfaccdef2d8 100644
--- a/fs/exfat/dir.c
+++ b/fs/exfat/dir.c
@@ -8,10 +8,19 @@
#include <linux/bio.h>
#include <linux/buffer_head.h>
#include <linux/filelock.h>
+#include <linux/hash.h>
+#include <linux/log2.h>
+#include <linux/stringhash.h>
#include "exfat_raw.h"
#include "exfat_fs.h"
+struct exfat_name_filter {
+ u32 nr_set_bits;
+ u8 order;
+ unsigned long bitmap[];
+};
+
static int exfat_extract_uni_name(struct exfat_dentry *ep,
unsigned short *uniname)
{
@@ -65,6 +74,178 @@ static int exfat_get_uniname_from_ext_entry(struct super_block *sb,
return 0;
}
+static u32 exfat_name_filter_hash(struct super_block *sb,
+ const struct exfat_uni_name *name)
+{
+ unsigned long hash = init_name_hash(NULL);
+ int i;
+
+ for (i = 0; i < name->name_len; i++)
+ hash = partial_name_hash(exfat_toupper(sb, name->name[i]), hash);
+
+ return end_name_hash(hash);
+}
+
+static void exfat_name_filter_indexes(struct super_block *sb,
+ const struct exfat_uni_name *name,
+ u8 order,
+ unsigned int indexes[3])
+{
+ u32 hash = exfat_name_filter_hash(sb, name);
+
+ indexes[0] = hash_32(hash, order);
+ indexes[1] = hash_32(hash ^ 0x9e3779b9U, order);
+ indexes[2] = hash_32(rol32(hash, 16) ^ 0x85ebca6bU,
+ order);
+}
+
+static u8 exfat_name_filter_order(struct inode *inode)
+{
+ u64 dentries = exfat_bytes_to_dentries(i_size_read(inode));
+ u64 bits;
+
+ dentries = clamp_t(u64, dentries,
+ EXFAT_NAME_FILTER_MIN_DENTRIES,
+ MAX_EXFAT_DENTRIES);
+ bits = dentries * EXFAT_NAME_FILTER_BITS_PER_DENTRY;
+
+ return clamp_t(unsigned int, fls64(bits - 1),
+ EXFAT_NAME_FILTER_MIN_ORDER,
+ EXFAT_NAME_FILTER_MAX_ORDER);
+}
+
+static bool exfat_name_filter_set(struct exfat_name_filter *filter,
+ const unsigned int indexes[3])
+{
+ unsigned long nr_bits = BIT(filter->order);
+ int i;
+
+ for (i = 0; i < 3; i++)
+ if (!__test_and_set_bit(indexes[i], filter->bitmap))
+ filter->nr_set_bits++;
+
+ return filter->nr_set_bits >= nr_bits / 3;
+}
+
+void exfat_name_filter_free(struct inode *inode)
+{
+ struct exfat_inode_info *ei = EXFAT_I(inode);
+
+ kvfree(ei->name_filter);
+ ei->name_filter = NULL;
+}
+
+bool exfat_name_filter_maybe_contains(struct inode *inode,
+ const struct exfat_uni_name *name)
+{
+ struct exfat_inode_info *ei = EXFAT_I(inode);
+ struct exfat_name_filter *filter = ei->name_filter;
+ unsigned int indexes[3];
+
+ if (!filter)
+ return true;
+
+ exfat_name_filter_indexes(inode->i_sb, name, filter->order, indexes);
+ return test_bit(indexes[0], filter->bitmap) &&
+ test_bit(indexes[1], filter->bitmap) &&
+ test_bit(indexes[2], filter->bitmap);
+}
+
+void exfat_name_filter_add(struct inode *inode,
+ const struct exfat_uni_name *name)
+{
+ struct exfat_inode_info *ei = EXFAT_I(inode);
+ struct exfat_name_filter *filter = ei->name_filter;
+ unsigned int indexes[3];
+
+ if (!filter)
+ return;
+
+ if (exfat_name_filter_order(inode) > filter->order) {
+ exfat_name_filter_free(inode);
+ return;
+ }
+
+ exfat_name_filter_indexes(inode->i_sb, name, filter->order, indexes);
+ if (exfat_name_filter_set(filter, indexes))
+ exfat_name_filter_free(inode);
+}
+
+/*
+ * Build a complete filter only after a directory becomes large enough for
+ * repeated negative linear lookups to matter. A filter hit is never trusted:
+ * it only allows definite misses to skip the on-disk scan.
+ */
+static void exfat_build_name_filter(struct super_block *sb,
+ struct exfat_inode_info *ei,
+ struct exfat_chain *p_dir)
+{
+ struct exfat_name_filter *filter;
+ struct exfat_chain clu;
+ unsigned int clu_count = 0;
+ struct inode *inode = &ei->vfs_inode;
+ struct exfat_sb_info *sbi = EXFAT_SB(sb);
+ u8 order;
+ int i;
+
+ if (ei->name_filter ||
+ exfat_bytes_to_dentries(i_size_read(inode)) <
+ EXFAT_NAME_FILTER_MIN_DENTRIES)
+ return;
+
+ order = exfat_name_filter_order(inode);
+ filter = kvzalloc(struct_size(filter, bitmap,
+ BITS_TO_LONGS(BIT(order))), GFP_NOFS);
+ if (!filter)
+ return;
+ filter->order = order;
+
+ exfat_chain_dup(&clu, p_dir);
+ while (clu.dir != EXFAT_EOF_CLUSTER) {
+ for (i = 0; i < sbi->dentries_per_clu; i++) {
+ struct exfat_uni_name name = { };
+ struct exfat_dentry *ep;
+ struct buffer_head *bh;
+ unsigned int type;
+ unsigned int indexes[3];
+ int len;
+
+ ep = exfat_get_dentry(sb, &clu, i, &bh);
+ if (!ep)
+ goto abort;
+
+ type = exfat_get_entry_type(ep);
+ brelse(bh);
+ if (type == TYPE_UNUSED)
+ goto complete;
+ if (type != TYPE_FILE && type != TYPE_DIR)
+ continue;
+
+ if (exfat_get_uniname_from_ext_entry(sb, &clu, i, name.name))
+ goto abort;
+ for (len = 0; len <= MAX_NAME_LENGTH && name.name[len]; len++)
+ ;
+ if (!len || len > MAX_NAME_LENGTH)
+ goto abort;
+ name.name_len = len;
+ exfat_name_filter_indexes(sb, &name, order, indexes);
+ if (exfat_name_filter_set(filter, indexes))
+ goto abort;
+ }
+
+ if (exfat_chain_advance(sb, &clu, 1))
+ goto abort;
+ if (unlikely(++clu_count > EXFAT_DATA_CLUSTER_COUNT(sbi)))
+ goto abort;
+ }
+
+complete:
+ ei->name_filter = filter;
+ return;
+abort:
+ kvfree(filter);
+}
+
/* read a directory entry from the opened directory */
static int exfat_readdir(struct inode *inode, loff_t *cpos, struct exfat_dir_entry *dir_entry)
{
@@ -1035,6 +1216,8 @@ int exfat_find_dir_entry(struct super_block *sb, struct exfat_inode_info *ei,
if (num_entries < 0)
return num_entries;
+ if (!exfat_name_filter_maybe_contains(&ei->vfs_inode, p_uniname))
+ return -ENOENT;
dentries_per_clu = sbi->dentries_per_clu;
@@ -1196,6 +1379,8 @@ int exfat_find_dir_entry(struct super_block *sb, struct exfat_inode_info *ei,
ei->hint_femp.count = 0;
}
+ exfat_build_name_filter(sb, ei, p_dir);
+
/* initialized hint_stat */
hint_stat->clu = p_dir->dir;
hint_stat->eidx = 0;
diff --git a/fs/exfat/exfat_fs.h b/fs/exfat/exfat_fs.h
index 43c034532ec5..015860883fa8 100644
--- a/fs/exfat/exfat_fs.h
+++ b/fs/exfat/exfat_fs.h
@@ -121,6 +121,11 @@ enum {
#define DIR_CACHE_SIZE \
(DIV_ROUND_UP(ES_MAX_ENTRY_NUM << DENTRY_SIZE_BITS, SECTOR_SIZE) + 1)
+#define EXFAT_NAME_FILTER_MIN_DENTRIES 1024
+#define EXFAT_NAME_FILTER_BITS_PER_DENTRY 4
+#define EXFAT_NAME_FILTER_MIN_ORDER 12
+#define EXFAT_NAME_FILTER_MAX_ORDER 25
+
/* Superblock flags */
#define EXFAT_FLAGS_SHUTDOWN 1
@@ -137,6 +142,8 @@ struct exfat_uni_name {
unsigned char name_len;
};
+struct exfat_name_filter;
+
/* directory structure */
struct exfat_chain {
unsigned int dir;
@@ -286,6 +293,8 @@ struct exfat_inode_info {
struct exfat_hint hint_stat;
/* hint for first empty entry */
struct exfat_hint_femp hint_femp;
+ /* Complete, in-memory Bloom filter of directory names */
+ struct exfat_name_filter *name_filter;
spinlock_t cache_lru_lock;
struct list_head cache_lru;
@@ -676,6 +685,12 @@ int exfat_read_volume_label(struct super_block *sb,
int exfat_write_volume_label(struct super_block *sb,
struct exfat_uni_name *label);
+bool exfat_name_filter_maybe_contains(struct inode *inode,
+ const struct exfat_uni_name *name);
+void exfat_name_filter_add(struct inode *inode,
+ const struct exfat_uni_name *name);
+void exfat_name_filter_free(struct inode *inode);
+
static inline int exfat_chain_advance(struct super_block *sb,
struct exfat_chain *chain, unsigned int step)
{
diff --git a/fs/exfat/inode.c b/fs/exfat/inode.c
index 5129df1e79b4..fde110da74ee 100644
--- a/fs/exfat/inode.c
+++ b/fs/exfat/inode.c
@@ -447,6 +447,7 @@ struct inode *exfat_build_inode(struct super_block *sb,
void exfat_evict_inode(struct inode *inode)
{
truncate_inode_pages_final(&inode->i_data);
+ exfat_name_filter_free(inode);
if (!inode->i_nlink) {
i_size_write(inode, 0);
diff --git a/fs/exfat/namei.c b/fs/exfat/namei.c
index 86c8c18d363f..5e47ca1bf722 100644
--- a/fs/exfat/namei.c
+++ b/fs/exfat/namei.c
@@ -512,6 +512,7 @@ static int exfat_add_entry(struct inode *inode, const char *path,
if (ret) {
int cleanup_ret;
+ exfat_name_filter_free(inode);
cleanup_ret = exfat_get_dentry_set(&es, sb, &info->dir,
dentry, ES_ALL_ENTRIES);
if (!cleanup_ret) {
@@ -526,6 +527,7 @@ static int exfat_add_entry(struct inode *inode, const char *path,
}
info->entry = dentry;
+ exfat_name_filter_add(inode, &uniname);
info->flags = ALLOC_NO_FAT_CHAIN;
info->type = type;
@@ -1216,6 +1218,10 @@ static int __exfat_rename(struct inode *old_parent_inode,
ret = exfat_rename_file(new_parent_inode, &uni_name, ei);
else
ret = exfat_move_file(new_parent_inode, &uni_name, ei);
+ if (!ret)
+ exfat_name_filter_add(new_parent_inode, &uni_name);
+ else
+ exfat_name_filter_free(new_parent_inode);
if (!ret && new_inode) {
struct exfat_entry_set_cache es;
diff --git a/fs/exfat/super.c b/fs/exfat/super.c
index 4943cef97741..4924f0fad836 100644
--- a/fs/exfat/super.c
+++ b/fs/exfat/super.c
@@ -209,6 +209,7 @@ static struct inode *exfat_alloc_inode(struct super_block *sb)
if (!ei)
return NULL;
+ ei->name_filter = NULL;
return &ei->vfs_inode;
}
--
2.34.1
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v8 2/3] exfat: retain the next empty directory entry hint
2026-10-14 15:31 [PATCH v8 0/3] exfat: speed up file creation in large directories Yang Wen
2026-10-14 15:31 ` [PATCH v8 1/3] exfat: add a Bloom filter for negative name lookups Yang Wen
@ 2026-10-14 15:31 ` Yang Wen
2026-10-14 15:31 ` [PATCH v8 3/3] exfat: reclaim name filters under memory pressure Yang Wen
2 siblings, 0 replies; 4+ messages in thread
From: Yang Wen @ 2026-10-14 15:31 UTC (permalink / raw)
To: linkinjeon, sj1557.seo, chizhiling
Cc: yuezhang.mo, exfat, linux-kernel, Yang Wen
After a Bloom filter miss skips the name scan, empty-entry allocation can
still rescan the directory from the beginning.
Calculate the next empty-entry hint when a free entry set is found, but
publish it only after the entry set is committed. Record the minimum
entry-set size for which a saved hint is valid, so a later shorter name
rescans earlier entries and can reuse smaller holes.
Retain the hint while the name filter is active and invalidate it whenever
create rollback, unlink, rmdir, rename, or move can free entries. Also
discard hints collected by positive name lookups, since such lookups can
start at hint_stat and return before scanning the beginning of the
directory.
Track the remaining root-directory chain length during volume-label scans
so any empty-entry hint published by that path remains usable across a
cluster boundary.
This preserves the fast append path during bulk creation without allowing
partial or stale hints to skip reusable directory entries.
Signed-off-by: Yang Wen <anmuxixixi@gmail.com>
---
fs/exfat/dir.c | 25 +++++++++++---
fs/exfat/exfat_fs.h | 5 ++-
fs/exfat/namei.c | 81 +++++++++++++++++++++++++++++++++++++++++----
3 files changed, 99 insertions(+), 12 deletions(-)
diff --git a/fs/exfat/dir.c b/fs/exfat/dir.c
index dcfaccdef2d8..4c36fded2412 100644
--- a/fs/exfat/dir.c
+++ b/fs/exfat/dir.c
@@ -1154,6 +1154,7 @@ static inline void exfat_reset_empty_hint(struct exfat_hint_femp *hint_femp)
{
hint_femp->eidx = EXFAT_HINT_NONE;
hint_femp->count = 0;
+ hint_femp->min_entries = 0;
}
static inline void exfat_set_empty_hint(struct exfat_inode_info *ei,
@@ -1167,6 +1168,7 @@ static inline void exfat_set_empty_hint(struct exfat_inode_info *ei,
if (candi_empty->count == 0) {
candi_empty->cur = *clu;
candi_empty->eidx = dentry;
+ candi_empty->min_entries = num_entries;
}
if (entry_type == TYPE_UNUSED)
@@ -1377,6 +1379,7 @@ int exfat_find_dir_entry(struct super_block *sb, struct exfat_inode_info *ei,
ei->hint_femp.cur.dir = EXFAT_EOF_CLUSTER;
ei->hint_femp.eidx = p_dir->size * dentries_per_clu;
ei->hint_femp.count = 0;
+ ei->hint_femp.min_entries = num_entries;
}
exfat_build_name_filter(sb, ei, p_dir);
@@ -1387,6 +1390,8 @@ int exfat_find_dir_entry(struct super_block *sb, struct exfat_inode_info *ei,
return -ENOENT;
found:
+ exfat_reset_empty_hint(&ei->hint_femp);
+
/* next dentry we'll find is out of this cluster */
if (!((dentry + 1) & (dentries_per_clu - 1))) {
int ret = 0;
@@ -1453,16 +1458,19 @@ static int exfat_get_volume_label_dentry(struct super_block *sb,
{
int i;
int dentry = 0;
+ struct inode *inode = sb->s_root->d_inode;
unsigned int type;
struct exfat_sb_info *sbi = EXFAT_SB(sb);
struct exfat_hint_femp hint_femp;
- struct exfat_inode_info *ei = EXFAT_I(sb->s_root->d_inode);
+ struct exfat_inode_info *ei = EXFAT_I(inode);
struct exfat_chain clu;
struct exfat_dentry *ep;
struct buffer_head *bh;
hint_femp.eidx = EXFAT_HINT_NONE;
- exfat_chain_set(&clu, sbi->root_dir, 0, ALLOC_FAT_CHAIN);
+ exfat_chain_set(&clu, sbi->root_dir,
+ exfat_bytes_to_cluster(sbi, i_size_read(inode)),
+ ALLOC_FAT_CHAIN);
while (clu.dir != EXFAT_EOF_CLUSTER) {
for (i = 0; i < sbi->dentries_per_clu; i++, dentry++) {
@@ -1476,6 +1484,7 @@ static int exfat_get_volume_label_dentry(struct super_block *sb,
hint_femp.cur = clu;
hint_femp.eidx = dentry;
hint_femp.count = 1;
+ hint_femp.min_entries = 1;
}
}
@@ -1500,7 +1509,7 @@ static int exfat_get_volume_label_dentry(struct super_block *sb,
return 0;
}
- if (exfat_get_next_cluster(sb, &(clu.dir)))
+ if (exfat_chain_advance(sb, &clu, 1))
return -EIO;
}
@@ -1509,6 +1518,7 @@ static int exfat_get_volume_label_dentry(struct super_block *sb,
hint_femp.cur.dir = EXFAT_EOF_CLUSTER;
hint_femp.eidx = dentry;
hint_femp.count = 0;
+ hint_femp.min_entries = 1;
}
ei->hint_femp = hint_femp;
@@ -1563,7 +1573,9 @@ int exfat_write_volume_label(struct super_block *sb,
struct inode *root_inode = sb->s_root->d_inode;
struct exfat_entry_set_cache es;
struct exfat_chain clu;
+ struct exfat_hint_femp next_hint;
struct exfat_dentry *ep;
+ bool entry_allocated = false;
if (label->name_len > EXFAT_VOLUME_LABEL_LEN)
return -EINVAL;
@@ -1578,7 +1590,10 @@ int exfat_write_volume_label(struct super_block *sb,
goto unlock;
}
- ret = exfat_find_empty_entry(root_inode, &clu, 1, &es);
+ ret = exfat_find_empty_entry(root_inode, &clu, 1, &es,
+ &next_hint);
+ if (ret >= 0)
+ entry_allocated = true;
}
if (ret < 0)
@@ -1603,6 +1618,8 @@ int exfat_write_volume_label(struct super_block *sb,
es.modified = true;
ret = exfat_put_dentry_set(&es, IS_DIRSYNC(root_inode));
+ if (!ret && entry_allocated)
+ EXFAT_I(root_inode)->hint_femp = next_hint;
unlock:
mutex_unlock(&sbi->s_lock);
diff --git a/fs/exfat/exfat_fs.h b/fs/exfat/exfat_fs.h
index 015860883fa8..d00b3235cee9 100644
--- a/fs/exfat/exfat_fs.h
+++ b/fs/exfat/exfat_fs.h
@@ -157,6 +157,8 @@ struct exfat_hint_femp {
int eidx;
/* count of continuous empty entry */
int count;
+ /* minimum entry-set size for which this hint is safe */
+ int min_entries;
/* the cluster that first empty slot exists in */
struct exfat_chain cur;
};
@@ -639,7 +641,8 @@ extern const struct dentry_operations exfat_dentry_ops;
extern const struct dentry_operations exfat_utf8_dentry_ops;
int exfat_find_empty_entry(struct inode *inode,
struct exfat_chain *p_dir, int num_entries,
- struct exfat_entry_set_cache *es);
+ struct exfat_entry_set_cache *es,
+ struct exfat_hint_femp *next_hint);
/* cache.c */
int exfat_cache_init(void);
diff --git a/fs/exfat/namei.c b/fs/exfat/namei.c
index 5e47ca1bf722..d8ddfc8fff2a 100644
--- a/fs/exfat/namei.c
+++ b/fs/exfat/namei.c
@@ -199,6 +199,54 @@ const struct dentry_operations exfat_utf8_dentry_ops = {
.d_compare = exfat_utf8_d_cmp,
};
+static void exfat_set_next_empty_hint(struct inode *inode,
+ struct exfat_chain *p_dir, int dentry,
+ int num_entries,
+ struct exfat_entry_set_cache *es,
+ struct exfat_hint_femp *hint_femp)
+{
+ struct exfat_sb_info *sbi = EXFAT_SB(inode->i_sb);
+ unsigned int next = dentry + num_entries;
+ unsigned int total = exfat_cluster_to_dentries(sbi, p_dir->size);
+ struct exfat_chain cur;
+
+ hint_femp->min_entries = num_entries;
+
+ if (next >= total) {
+ exfat_chain_set(&hint_femp->cur, EXFAT_EOF_CLUSTER, 0,
+ p_dir->flags);
+ hint_femp->eidx = total;
+ hint_femp->count = 0;
+ return;
+ }
+
+ cur.dir = exfat_sector_to_cluster(sbi,
+ es->bh[es->num_bh - 1]->b_blocknr);
+ cur.flags = p_dir->flags;
+ cur.size = p_dir->size - exfat_dentries_to_cluster(sbi, next);
+ if (!(next & (sbi->dentries_per_clu - 1))) {
+ cur.size++;
+ if (exfat_chain_advance(inode->i_sb, &cur, 1)) {
+ hint_femp->eidx = EXFAT_HINT_NONE;
+ hint_femp->count = 0;
+ return;
+ }
+ }
+
+ hint_femp->cur = cur;
+ hint_femp->eidx = next;
+ hint_femp->count = 0;
+}
+
+static void exfat_invalidate_empty_hint(struct inode *inode)
+{
+ struct exfat_inode_info *ei = EXFAT_I(inode);
+
+ ei->hint_femp.eidx = EXFAT_HINT_NONE;
+ ei->hint_femp.count = 0;
+ ei->hint_femp.min_entries = 0;
+}
+
/* search EMPTY CONTINUOUS "num_entries" entries */
static int exfat_search_empty_slot(struct super_block *sb,
struct exfat_hint_femp *hint_femp, struct exfat_chain *p_dir,
@@ -295,7 +343,8 @@ static int exfat_check_max_dentries(struct inode *inode)
*/
int exfat_find_empty_entry(struct inode *inode,
struct exfat_chain *p_dir, int num_entries,
- struct exfat_entry_set_cache *es)
+ struct exfat_entry_set_cache *es,
+ struct exfat_hint_femp *next_hint)
{
int dentry, ret;
unsigned int last_clu;
@@ -309,8 +358,9 @@ int exfat_find_empty_entry(struct inode *inode,
hint_femp.eidx = EXFAT_HINT_NONE;
if (ei->hint_femp.eidx != EXFAT_HINT_NONE) {
- hint_femp = ei->hint_femp;
- ei->hint_femp.eidx = EXFAT_HINT_NONE;
+ if (num_entries >= ei->hint_femp.min_entries)
+ hint_femp = ei->hint_femp;
+ exfat_invalidate_empty_hint(inode);
}
exfat_chain_set(p_dir, ei->start_clu,
@@ -385,6 +435,9 @@ int exfat_find_empty_entry(struct inode *inode,
inode->i_blocks += sbi->cluster_size >> 9;
}
+ exfat_set_next_empty_hint(inode, p_dir, dentry, num_entries, es,
+ next_hint);
+
p_dir->dir = exfat_sector_to_cluster(sbi, es->bh[0]->b_blocknr);
p_dir->size -= dentry >> sbi->dentries_per_clu_bits;
@@ -469,6 +522,7 @@ static int exfat_add_entry(struct inode *inode, const char *path,
struct exfat_chain clu;
struct timespec64 ts = current_time(inode);
struct exfat_entry_set_cache es;
+ struct exfat_hint_femp next_hint;
int clu_size = 0;
unsigned int start_clu = EXFAT_FREE_CLUSTER;
bool dir_allocated = false;
@@ -484,7 +538,8 @@ static int exfat_add_entry(struct inode *inode, const char *path,
}
/* exfat_find_empty_entry must be called before alloc_cluster() */
- dentry = exfat_find_empty_entry(inode, &info->dir, num_entries, &es);
+ dentry = exfat_find_empty_entry(inode, &info->dir, num_entries, &es,
+ &next_hint);
if (dentry < 0) {
ret = dentry; /* -EIO or -ENOSPC */
goto out;
@@ -517,6 +572,7 @@ static int exfat_add_entry(struct inode *inode, const char *path,
dentry, ES_ALL_ENTRIES);
if (!cleanup_ret) {
exfat_remove_entries(inode, &es, ES_IDX_FILE, false);
+ exfat_invalidate_empty_hint(inode);
cleanup_ret = exfat_put_dentry_set(&es,
IS_DIRSYNC(inode));
}
@@ -525,6 +581,7 @@ static int exfat_add_entry(struct inode *inode, const char *path,
exfat_free_cluster(inode, &clu);
goto out;
}
+ EXFAT_I(inode)->hint_femp = next_hint;
info->entry = dentry;
exfat_name_filter_add(inode, &uniname);
@@ -629,7 +686,8 @@ static int exfat_find(struct inode *dir, const struct qstr *qname,
ei->hint_stat.clu = cdir.dir;
ei->hint_stat.eidx = 0;
ei->version = (inode_peek_iversion_raw(dir) & 0xffffffff);
- ei->hint_femp.eidx = EXFAT_HINT_NONE;
+ if (!ei->name_filter)
+ ei->hint_femp.eidx = EXFAT_HINT_NONE;
}
/* search the file name for directories */
@@ -805,6 +863,7 @@ static int exfat_unlink(struct inode *dir, struct dentry *dentry)
/* update the directory entry */
exfat_remove_entries(inode, &es, ES_IDX_FILE, true);
+ exfat_invalidate_empty_hint(dir);
err = exfat_put_dentry_set(&es, IS_DIRSYNC(inode));
if (err)
@@ -960,6 +1019,7 @@ static int exfat_rmdir(struct inode *dir, struct dentry *dentry)
exfat_set_volume_dirty(sb);
exfat_remove_entries(inode, &es, ES_IDX_FILE, true);
+ exfat_invalidate_empty_hint(dir);
err = exfat_put_dentry_set(&es, IS_DIRSYNC(dir));
if (err)
@@ -1035,9 +1095,10 @@ static int exfat_rename_file(struct inode *parent_inode,
if (old_es.num_entries < num_total_entries) {
int newentry;
struct exfat_chain dir;
+ struct exfat_hint_femp next_hint;
newentry = exfat_find_empty_entry(parent_inode, &dir,
- num_total_entries, &new_es);
+ num_total_entries, &new_es, &next_hint);
if (newentry < 0) {
ret = newentry; /* -EIO or -ENOSPC */
goto put_old_es;
@@ -1069,6 +1130,7 @@ static int exfat_rename_file(struct inode *parent_inode,
}
goto put_old_es;
}
+ EXFAT_I(parent_inode)->hint_femp = next_hint;
exfat_remove_entries(parent_inode, &old_es, ES_IDX_FILE, false);
ei->dir = dir;
@@ -1096,6 +1158,7 @@ static int exfat_move_file(struct inode *parent_inode,
struct exfat_dentry *epmov, *epnew;
struct exfat_entry_set_cache mov_es, new_es;
struct exfat_chain newdir;
+ struct exfat_hint_femp next_hint;
unsigned int num_extra_entries, num_total_entries;
num_new_entries = exfat_calc_num_entries(p_uniname);
@@ -1113,7 +1176,7 @@ static int exfat_move_file(struct inode *parent_inode,
num_total_entries = num_new_entries + num_extra_entries;
newentry = exfat_find_empty_entry(parent_inode, &newdir,
- num_total_entries, &new_es);
+ num_total_entries, &new_es, &next_hint);
if (newentry < 0) {
ret = newentry; /* -EIO or -ENOSPC */
goto put_mov_es;
@@ -1142,10 +1205,12 @@ static int exfat_move_file(struct inode *parent_inode,
ES_ALL_ENTRIES)) {
exfat_remove_entries(parent_inode, &new_es,
ES_IDX_FILE, false);
+ exfat_invalidate_empty_hint(parent_inode);
exfat_put_dentry_set(&new_es, false);
}
goto put_mov_es;
}
+ EXFAT_I(parent_inode)->hint_femp = next_hint;
exfat_remove_entries(parent_inode, &mov_es, ES_IDX_FILE, false);
@@ -1218,6 +1283,7 @@ static int __exfat_rename(struct inode *old_parent_inode,
ret = exfat_rename_file(new_parent_inode, &uni_name, ei);
else
ret = exfat_move_file(new_parent_inode, &uni_name, ei);
+ exfat_invalidate_empty_hint(old_parent_inode);
if (!ret)
exfat_name_filter_add(new_parent_inode, &uni_name);
else
@@ -1234,6 +1300,7 @@ static int __exfat_rename(struct inode *old_parent_inode,
}
exfat_remove_entries(new_inode, &es, ES_IDX_FILE, true);
+ exfat_invalidate_empty_hint(new_parent_inode);
ret = exfat_put_dentry_set(&es, IS_DIRSYNC(new_inode));
if (ret)
--
2.34.1
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v8 3/3] exfat: reclaim name filters under memory pressure
2026-10-14 15:31 [PATCH v8 0/3] exfat: speed up file creation in large directories Yang Wen
2026-10-14 15:31 ` [PATCH v8 1/3] exfat: add a Bloom filter for negative name lookups Yang Wen
2026-10-14 15:31 ` [PATCH v8 2/3] exfat: retain the next empty directory entry hint Yang Wen
@ 2026-10-14 15:31 ` Yang Wen
2 siblings, 0 replies; 4+ messages in thread
From: Yang Wen @ 2026-10-14 15:31 UTC (permalink / raw)
To: linkinjeon, sj1557.seo, chizhiling
Cc: yuezhang.mo, exfat, linux-kernel, Yang Wen
Each active large directory can hold a dynamically sized name filter with a
bitmap of up to 4 MiB. Add a per-superblock LRU and shrinker so filters can
be reclaimed when memory is tight. Account reclaimable filters in pages so
reclaim cost tracks their differing allocation sizes.
Keep the scan budget initialized by the shrinker core instead of replacing
it with the number of filters freed. If the LRU is already empty, return
SHRINK_STOP. This guarantees that do_shrink_slab() either consumes its scan
budget or stops, even when the LRU becomes empty before nr_to_scan is
satisfied.
Signed-off-by: Yang Wen <anmuxixixi@gmail.com>
---
fs/exfat/dir.c | 159 +++++++++++++++++++++++++++++++++++++++++++-
fs/exfat/exfat_fs.h | 10 +++
fs/exfat/super.c | 8 +++
3 files changed, 174 insertions(+), 3 deletions(-)
diff --git a/fs/exfat/dir.c b/fs/exfat/dir.c
index 4c36fded2412..bc99f2d6739e 100644
--- a/fs/exfat/dir.c
+++ b/fs/exfat/dir.c
@@ -10,6 +10,7 @@
#include <linux/filelock.h>
#include <linux/hash.h>
#include <linux/log2.h>
+#include <linux/shrinker.h>
#include <linux/stringhash.h>
#include "exfat_raw.h"
@@ -127,12 +128,158 @@ static bool exfat_name_filter_set(struct exfat_name_filter *filter,
return filter->nr_set_bits >= nr_bits / 3;
}
+static unsigned long
+exfat_name_filter_nr_pages(const struct exfat_name_filter *filter)
+{
+ size_t size = struct_size(filter, bitmap,
+ BITS_TO_LONGS(BIT(filter->order)));
+
+ return DIV_ROUND_UP(size, PAGE_SIZE);
+}
+
+static struct exfat_name_filter *
+exfat_name_filter_detach_locked(struct exfat_sb_info *sbi,
+ struct exfat_inode_info *ei)
+{
+ struct exfat_name_filter *filter = ei->name_filter;
+
+ if (!filter)
+ return NULL;
+
+ list_del_init(&ei->name_filter_lru);
+ ei->name_filter = NULL;
+ sbi->name_filter_pages -= exfat_name_filter_nr_pages(filter);
+ return filter;
+}
+
+static void exfat_name_filter_touch(struct exfat_inode_info *ei)
+{
+ struct exfat_sb_info *sbi = EXFAT_SB(ei->vfs_inode.i_sb);
+
+ spin_lock(&sbi->name_filter_lock);
+ if (ei->name_filter)
+ list_move_tail(&ei->name_filter_lru, &sbi->name_filter_lru);
+ spin_unlock(&sbi->name_filter_lock);
+}
+
void exfat_name_filter_free(struct inode *inode)
{
struct exfat_inode_info *ei = EXFAT_I(inode);
+ struct exfat_sb_info *sbi;
+ struct exfat_name_filter *filter;
- kvfree(ei->name_filter);
- ei->name_filter = NULL;
+ if (!READ_ONCE(ei->name_filter))
+ return;
+
+ sbi = EXFAT_SB(inode->i_sb);
+ spin_lock(&sbi->name_filter_lock);
+ filter = exfat_name_filter_detach_locked(sbi, ei);
+ spin_unlock(&sbi->name_filter_lock);
+ kvfree(filter);
+}
+
+static unsigned long
+exfat_name_filter_count_objects(struct shrinker *shrinker,
+ struct shrink_control *sc)
+{
+ struct exfat_sb_info *sbi = shrinker->private_data;
+ unsigned long pages;
+
+ spin_lock(&sbi->name_filter_lock);
+ pages = sbi->name_filter_pages;
+ spin_unlock(&sbi->name_filter_lock);
+
+ return pages ? pages : SHRINK_EMPTY;
+}
+
+static unsigned long
+exfat_name_filter_scan_objects(struct shrinker *shrinker,
+ struct shrink_control *sc)
+{
+ struct exfat_sb_info *sbi = shrinker->private_data;
+ unsigned long freed = 0;
+ bool empty = false;
+
+ /* Avoid reclaim recursion from a GFP_NOFS allocation under s_lock. */
+ if (!mutex_trylock(&sbi->s_lock)) {
+ sc->nr_scanned = 0;
+ return SHRINK_STOP;
+ }
+
+ while (freed < sc->nr_to_scan) {
+ struct exfat_inode_info *ei;
+ struct exfat_name_filter *filter;
+
+ spin_lock(&sbi->name_filter_lock);
+ if (list_empty(&sbi->name_filter_lru)) {
+ spin_unlock(&sbi->name_filter_lock);
+ empty = true;
+ break;
+ }
+
+ ei = list_first_entry(&sbi->name_filter_lru,
+ struct exfat_inode_info,
+ name_filter_lru);
+ filter = exfat_name_filter_detach_locked(sbi, ei);
+ spin_unlock(&sbi->name_filter_lock);
+
+ freed += exfat_name_filter_nr_pages(filter);
+ kvfree(filter);
+ cond_resched();
+ }
+
+ mutex_unlock(&sbi->s_lock);
+ if (empty && !freed) {
+ sc->nr_scanned = 0;
+ return SHRINK_STOP;
+ }
+ return freed;
+}
+
+void exfat_name_filter_shrinker_register(struct super_block *sb)
+{
+ struct exfat_sb_info *sbi = EXFAT_SB(sb);
+ struct shrinker *shrinker;
+
+ shrinker = shrinker_alloc(SHRINKER_NONSLAB,
+ "exfat-name-filter:%s", sb->s_id);
+ if (!shrinker) {
+ exfat_warn(sb, "failed to allocate name filter shrinker");
+ return;
+ }
+
+ shrinker->count_objects = exfat_name_filter_count_objects;
+ shrinker->scan_objects = exfat_name_filter_scan_objects;
+ shrinker->private_data = sbi;
+ shrinker_register(shrinker);
+ sbi->name_filter_shrinker = shrinker;
+}
+
+void exfat_name_filter_shrinker_unregister(struct super_block *sb)
+{
+ struct exfat_sb_info *sbi = EXFAT_SB(sb);
+ struct shrinker *shrinker = sbi->name_filter_shrinker;
+
+ sbi->name_filter_shrinker = NULL;
+ shrinker_free(shrinker);
+
+ for (;;) {
+ struct exfat_inode_info *ei;
+ struct exfat_name_filter *filter;
+
+ spin_lock(&sbi->name_filter_lock);
+ if (list_empty(&sbi->name_filter_lru)) {
+ spin_unlock(&sbi->name_filter_lock);
+ break;
+ }
+
+ ei = list_first_entry(&sbi->name_filter_lru,
+ struct exfat_inode_info,
+ name_filter_lru);
+ filter = exfat_name_filter_detach_locked(sbi, ei);
+ spin_unlock(&sbi->name_filter_lock);
+ kvfree(filter);
+ }
}
bool exfat_name_filter_maybe_contains(struct inode *inode,
@@ -145,6 +292,7 @@ bool exfat_name_filter_maybe_contains(struct inode *inode,
if (!filter)
return true;
+ exfat_name_filter_touch(ei);
exfat_name_filter_indexes(inode->i_sb, name, filter->order, indexes);
return test_bit(indexes[0], filter->bitmap) &&
test_bit(indexes[1], filter->bitmap) &&
@@ -161,6 +309,7 @@ void exfat_name_filter_add(struct inode *inode,
if (!filter)
return;
+ exfat_name_filter_touch(ei);
if (exfat_name_filter_order(inode) > filter->order) {
exfat_name_filter_free(inode);
return;
@@ -188,7 +337,7 @@ static void exfat_build_name_filter(struct super_block *sb,
u8 order;
int i;
- if (ei->name_filter ||
+ if (!sbi->name_filter_shrinker || ei->name_filter ||
exfat_bytes_to_dentries(i_size_read(inode)) <
EXFAT_NAME_FILTER_MIN_DENTRIES)
return;
@@ -240,7 +389,11 @@ static void exfat_build_name_filter(struct super_block *sb,
}
complete:
+ spin_lock(&sbi->name_filter_lock);
ei->name_filter = filter;
+ list_add_tail(&ei->name_filter_lru, &sbi->name_filter_lru);
+ sbi->name_filter_pages += exfat_name_filter_nr_pages(filter);
+ spin_unlock(&sbi->name_filter_lock);
return;
abort:
kvfree(filter);
diff --git a/fs/exfat/exfat_fs.h b/fs/exfat/exfat_fs.h
index d00b3235cee9..472e341e4d7d 100644
--- a/fs/exfat/exfat_fs.h
+++ b/fs/exfat/exfat_fs.h
@@ -15,6 +15,8 @@
#include <uapi/linux/exfat.h>
#include <linux/buffer_head.h>
+struct shrinker;
+
#define EXFAT_ROOT_INO 1
/*
@@ -266,6 +268,11 @@ struct exfat_sb_info {
spinlock_t inode_hash_lock;
struct hlist_head inode_hashtable[EXFAT_HASH_SIZE];
+ /* Protects name_filter_lru and name_filter_pages. */
+ spinlock_t name_filter_lock;
+ struct list_head name_filter_lru;
+ unsigned long name_filter_pages;
+ struct shrinker *name_filter_shrinker;
struct rcu_head rcu;
};
@@ -297,6 +304,7 @@ struct exfat_inode_info {
struct exfat_hint_femp hint_femp;
/* Complete, in-memory Bloom filter of directory names */
struct exfat_name_filter *name_filter;
+ struct list_head name_filter_lru;
spinlock_t cache_lru_lock;
struct list_head cache_lru;
@@ -693,6 +701,8 @@ bool exfat_name_filter_maybe_contains(struct inode *inode,
void exfat_name_filter_add(struct inode *inode,
const struct exfat_uni_name *name);
void exfat_name_filter_free(struct inode *inode);
+void exfat_name_filter_shrinker_register(struct super_block *sb);
+void exfat_name_filter_shrinker_unregister(struct super_block *sb);
static inline int exfat_chain_advance(struct super_block *sb,
struct exfat_chain *chain, unsigned int step)
diff --git a/fs/exfat/super.c b/fs/exfat/super.c
index 4924f0fad836..26b25cc86d1c 100644
--- a/fs/exfat/super.c
+++ b/fs/exfat/super.c
@@ -46,6 +46,7 @@ static void exfat_put_super(struct super_block *sb)
{
struct exfat_sb_info *sbi = EXFAT_SB(sb);
+ exfat_name_filter_shrinker_unregister(sb);
mutex_lock(&sbi->s_lock);
exfat_clear_volume_dirty(sb);
exfat_free_bitmap(sbi);
@@ -210,6 +211,7 @@ static struct inode *exfat_alloc_inode(struct super_block *sb)
return NULL;
ei->name_filter = NULL;
+ INIT_LIST_HEAD(&ei->name_filter_lru);
return &ei->vfs_inode;
}
@@ -746,6 +748,8 @@ static int exfat_fill_super(struct super_block *sb, struct fs_context *fc)
goto free_table;
}
+ exfat_name_filter_shrinker_register(sb);
+
return 0;
put_inode:
@@ -845,6 +849,10 @@ static int exfat_init_fs_context(struct fs_context *fc)
mutex_init(&sbi->s_lock);
mutex_init(&sbi->bitmap_lock);
+ spin_lock_init(&sbi->name_filter_lock);
+ INIT_LIST_HEAD(&sbi->name_filter_lru);
+ sbi->name_filter_pages = 0;
+ sbi->name_filter_shrinker = NULL;
ratelimit_state_init(&sbi->ratelimit, DEFAULT_RATELIMIT_INTERVAL,
DEFAULT_RATELIMIT_BURST);
--
2.34.1
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-10-14 15:32 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-14 15:31 [PATCH v8 0/3] exfat: speed up file creation in large directories Yang Wen
2026-10-14 15:31 ` [PATCH v8 1/3] exfat: add a Bloom filter for negative name lookups Yang Wen
2026-10-14 15:31 ` [PATCH v8 2/3] exfat: retain the next empty directory entry hint Yang Wen
2026-10-14 15:31 ` [PATCH v8 3/3] exfat: reclaim name filters under memory pressure Yang Wen
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®