Index: common/Profiler.h
===================================================================
--- common/Profiler.h	(revision 24593)
+++ common/Profiler.h	(working copy)
@@ -54,7 +54,7 @@
 
 #if defined(MODPLUG_TRACKER)
 
-//#define USE_PROFILER
+#define USE_PROFILER
 
 #endif
 
Index: mptrack/Mptrack.cpp
===================================================================
--- mptrack/Mptrack.cpp	(revision 24593)
+++ mptrack/Mptrack.cpp	(working copy)
@@ -932,7 +932,7 @@
 
 const mpt::PathString &CTrackApp::GetSongSettingsFilename() const
 {
-	return m_pSongSettingsIniFile->GetFilename();
+	return m_pSongSettingsIniFile->Filename();
 }
 
 
Index: mptrack/SettingsIni.cpp
===================================================================
--- mptrack/SettingsIni.cpp	(revision 24593)
+++ mptrack/SettingsIni.cpp	(working copy)
@@ -13,6 +13,7 @@
 
 #include "SettingsIni.h"
 
+#include "mpt/binary/hex.hpp"
 #include "mpt/io_file/fstream.hpp"
 #include "mpt/io_file/outputfile.hpp"
 #include "mpt/parse/parse.hpp"
@@ -22,11 +23,33 @@
 #include "../common/mptStringBuffer.h"
 
 #include <algorithm>
+#include <utility>
 
 
+
 OPENMPT_NAMESPACE_BEGIN
 
 
+
+WindowsIniFileBase::WindowsIniFileBase(mpt::PathString filename)
+	: filename(std::move(filename))
+	, file(filename)
+{
+	return;
+}
+
+const mpt::PathString &WindowsIniFileBase::Filename() const
+{
+	return filename;
+}
+
+mpt::PathString WindowsIniFileBase::GetFilename() const
+{
+	return filename;
+}
+
+
+
 std::vector<std::byte> ImmediateWindowsIniFileSettingsBackend::ReadSettingRaw(const SettingPath &path, const std::vector<std::byte> &def) const
 {
 	std::vector<std::byte> result = def;
@@ -126,12 +149,17 @@
 	::WritePrivateProfileString(mpt::ToWin(section).c_str(), NULL, NULL, filename.AsNative().c_str());
 }
 
+void CachedBatchedWindowsIniFileSettingsBackend::RemoveSectionRaw(const mpt::ustring &section)
+{
+	::WritePrivateProfileString(mpt::ToWin(section).c_str(), NULL, NULL, filename.AsNative().c_str());
+}
 
-mpt::winstring ImmediateWindowsIniFileSettingsBackend::GetSection(const SettingPath &path)
+
+mpt::winstring IniFileHelpers::GetSection(const SettingPath &path)
 {
 	return mpt::ToWin(path.GetSection());
 }
-mpt::winstring ImmediateWindowsIniFileSettingsBackend::GetKey(const SettingPath &path)
+mpt::winstring IniFileHelpers::GetKey(const SettingPath &path)
 {
 	return mpt::ToWin(path.GetKey());
 }
@@ -138,18 +166,36 @@
 
 
 
-ImmediateWindowsIniFileSettingsBackend::ImmediateWindowsIniFileSettingsBackend(const mpt::PathString &filename)
-	: filename(filename)
-	, file(filename)
+ImmediateWindowsIniFileSettingsBackend::ImmediateWindowsIniFileSettingsBackend(mpt::PathString filename)
+	: WindowsIniFileBase(std::move(filename))
 {
+	OPENMPT_PROFILE_FUNCTION(Profiler::Settings);
 	return;
 }
 
+CachedBatchedWindowsIniFileSettingsBackend::CachedBatchedWindowsIniFileSettingsBackend(mpt::PathString filename)
+	: WindowsIniFileBase(std::move(filename))
+{
+	OPENMPT_PROFILE_FUNCTION(Profiler::Settings);
+	ConvertToUnicode();
+	cache = ReadAllSectionsRaw();
+}
+
+void CachedBatchedWindowsIniFileSettingsBackend::InvalidateCache()
+{
+	cache = ReadAllSectionsRaw();
+}
+
 ImmediateWindowsIniFileSettingsBackend::~ImmediateWindowsIniFileSettingsBackend()
 {
 	return;
 }
 
+CachedBatchedWindowsIniFileSettingsBackend::~CachedBatchedWindowsIniFileSettingsBackend()
+{
+	return;
+}
+
 static void WriteFileUTF16LE(mpt::IO::atomic_shared_file_ref &file, const std::wstring &str)
 {
 	static_assert(sizeof(wchar_t) == 2);
@@ -160,7 +206,7 @@
 	file.write(mpt::byte_cast<mpt::const_byte_span>(mpt::as_span(inifile.str())));
 }
 
-void ImmediateWindowsIniFileSettingsBackend::ConvertToUnicode(const mpt::ustring &backupTag)
+void WindowsIniFileBase::ConvertToUnicode(const mpt::ustring &backupTag)
 {
 	// Force ini file to be encoded in UTF16.
 	// This causes WINAPI ini file functions to keep it in UTF16 encoding
@@ -209,6 +255,39 @@
 	}
 }
 
+SettingValue CachedBatchedWindowsIniFileSettingsBackend::ReadSetting(const SettingPath &path, const SettingValue &def) const
+{
+	OPENMPT_PROFILE_FUNCTION(Profiler::Settings);
+	const auto sectionit = cache.find(path.GetRefSection());
+	if(sectionit != cache.end())
+	{
+		if(sectionit->second.has_value())
+		{
+			const std::map<mpt::ustring, std::optional<mpt::ustring>> &section = sectionit->second.value();
+			const auto it = section.find(path.GetRefKey());
+			if(it != section.end())
+			{
+				if(it->second.has_value())
+				{
+					return ParseValueFromIni(it->second.value(), def);
+				} else
+				{
+					return def;
+				}
+			} else
+			{
+				return def;
+			}
+		} else
+		{
+			return def;
+		}
+	} else
+	{
+		return def;
+	}
+}
+
 void ImmediateWindowsIniFileSettingsBackend::WriteSetting(const SettingPath &path, const SettingValue &val)
 {
 	OPENMPT_PROFILE_FUNCTION(Profiler::Settings);
@@ -236,22 +315,271 @@
 	RemoveSectionRaw(section);
 }
 
+std::set<mpt::ustring> CachedBatchedWindowsIniFileSettingsBackend::ReadSectionNamesRaw() const
+{
+	std::set<mpt::ustring> result;
+	const std::vector<TCHAR> sectionsstr = [&]()
+		{
+			std::vector<TCHAR> buf;
+			buf.resize(mpt::IO::BUFFERSIZE_SMALL);
+			while(true)
+			{
+				DWORD bufused = ::GetPrivateProfileSectionNames(buf.data(), mpt::saturate_cast<DWORD>(buf.size()), filename.AsNative().c_str());
+				if(bufused >= (buf.size() - 2))
+				{
+					std::size_t newsize = mpt::exponential_grow(buf.size());
+					buf.resize(0);
+					buf.resize(newsize);
+					continue;
+				}
+				if(bufused >= 1)
+				{
+					bufused -= 1;  // terminating \0
+				}
+				buf.resize(bufused);
+				break;
+			};
+			return buf;
+		}();
+	const std::vector<mpt::winstring> sections = mpt::split(mpt::winstring(sectionsstr.data(), sectionsstr.size()), mpt::winstring(_T("\0"), 1));
+	for(const auto &section : sections)
+	{
+		result.insert(mpt::ToUnicode(section));
+	}
+	return result;
+}
 
+std::map<mpt::ustring, std::optional<mpt::ustring>> CachedBatchedWindowsIniFileSettingsBackend::ReadNamedSectionRaw(const mpt::ustring &section) const
+{
+	std::map<mpt::ustring, std::optional<mpt::ustring>> result;
+	const std::vector<TCHAR> keyvalsstr = [&]()
+		{
+			std::vector<TCHAR> buf;
+			buf.resize(mpt::IO::BUFFERSIZE_SMALL);
+			while(true)
+			{
+				DWORD bufused = ::GetPrivateProfileSection(mpt::ToWin(section).c_str(), buf.data(), mpt::saturate_cast<DWORD>(buf.size()), filename.AsNative().c_str());
+				if(bufused >= (buf.size() - 2))
+				{
+					std::size_t newsize = mpt::exponential_grow(buf.size());
+					buf.resize(0);
+					buf.resize(newsize);
+					continue;
+				}
+				if(bufused >= 1)
+				{
+					bufused -= 1;  // terminating \0
+				}
+				buf.resize(bufused);
+				break;
+			};
+			return buf;
+		}();
+	const std::vector<mpt::winstring> keyvals = mpt::split(mpt::winstring(keyvalsstr.data(), keyvalsstr.size()), mpt::winstring(_T("\0"), 1));
+	for(const auto &keyval : keyvals)
+	{
+		const auto equalpos = keyval.find(_T("="));
+		if(equalpos == mpt::winstring::npos)
+		{
+			continue;
+		}
+		if(equalpos == 0)
+		{
+			continue;
+		}
+		result.insert(std::make_pair(mpt::ToUnicode(keyval.substr(0, equalpos)), std::make_optional(mpt::ToUnicode(keyval.substr(equalpos + 1)))));
+	}
+	return result;
+}
 
+std::map<mpt::ustring, std::optional<std::map<mpt::ustring, std::optional<mpt::ustring>>>> CachedBatchedWindowsIniFileSettingsBackend::ReadAllSectionsRaw() const
+{
+	std::map<mpt::ustring, std::optional<std::map<mpt::ustring, std::optional<mpt::ustring>>>> result;
+	const std::set<mpt::ustring> sectionnames = ReadSectionNamesRaw();
+	for(const mpt::ustring &sectionname : sectionnames)
+	{
+		result.insert(std::make_pair(sectionname, std::make_optional(ReadNamedSectionRaw(sectionname))));
+	}
+	return result;
+}
 
+mpt::ustring IniFileHelpers::FormatValueAsIni(const SettingValue &value)
+{
+	switch(value.GetType())
+	{
+		case SettingTypeBool:
+			return mpt::ufmt::val(value.as<bool>());
+			break;
+		case SettingTypeInt:
+			return mpt::ufmt::val(value.as<int32>());
+			break;
+		case SettingTypeFloat:
+			return mpt::ufmt::val(value.as<double>());
+			break;
+		case SettingTypeString:
+			return value.as<mpt::ustring>();
+			break;
+		case SettingTypeBinary:
+			{
+				std::vector<std::byte> data = value.as<std::vector<std::byte>>();
+				uint8 checksum = 0;
+				for(const std::byte x : data) {
+					checksum += mpt::byte_cast<uint8>(x);
+				}
+				return mpt::encode_hex(mpt::as_span(data)) + mpt::ufmt::HEX0<2>(checksum);
+			}
+			break;
+		case SettingTypeNone:
+		default:
+			return mpt::ustring();
+			break;
+	}
+}
 
-IniFileSettingsContainer::IniFileSettingsContainer(const mpt::PathString &filename)
-	: ImmediateWindowsIniFileSettingsBackend(filename)
-	, SettingsContainer(this)
+
+SettingValue IniFileHelpers::ParseValueFromIni(const mpt::ustring &str, const SettingValue &def)
 {
+	switch(def.GetType())
+	{
+		case SettingTypeBool:
+			return SettingValue(mpt::parse_or<bool>(mpt::trim(str), def.as<bool>()), def.GetTypeTag());
+			break;
+		case SettingTypeInt:
+			return SettingValue(mpt::parse_or<int32>(mpt::trim(str), def.as<int32>()), def.GetTypeTag());
+			break;
+		case SettingTypeFloat:
+			return SettingValue(mpt::parse_or<double>(mpt::trim(str), def.as<double>()), def.GetTypeTag());
+			break;
+		case SettingTypeString:
+			return SettingValue(str, def.GetTypeTag());
+			break;
+		case SettingTypeBinary:
+			{
+				if((str.length() % 2) != 0)
+				{
+					return SettingValue(def.as<std::vector<std::byte>>(), def.GetTypeTag());
+				}
+				std::vector<std::byte> data = mpt::decode_hex(str);
+				if(data.size() < 1)
+				{
+					return SettingValue(def.as<std::vector<std::byte>>(), def.GetTypeTag());
+				}
+				const uint8 storedchecksum = mpt::byte_cast<uint8>(data[data.size() - 1]);
+				data.resize(data.size() - 1);
+				uint8 calculatedchecksum = 0;
+				for(const std::byte x : data) {
+					calculatedchecksum += mpt::byte_cast<uint8>(x);
+				}
+				if(calculatedchecksum != storedchecksum)
+				{
+					return SettingValue(def.as<std::vector<std::byte>>(), def.GetTypeTag());
+				}
+				return SettingValue(data, def.GetTypeTag());
+			}
+			break;
+		default:
+			return SettingValue();
+			break;
+	}
+}
+
+void CachedBatchedWindowsIniFileSettingsBackend::WriteSectionRaw(const mpt::ustring &section, const std::map<mpt::ustring, std::optional<mpt::ustring>> &keyvalues)
+{
+	OPENMPT_PROFILE_FUNCTION(Profiler::Settings);
+	mpt::winstring keyvals;
+	for(const auto &[key, val] : keyvalues)
+	{
+		if(val.has_value())
+		{
+			keyvals.append(mpt::ToWin(key));
+			keyvals.append(_T("="));
+			keyvals.append(mpt::ToWin(val.value()));
+			keyvals.append(mpt::winstring(_T("\0"), 1));
+		}
+	}
+	keyvals.append(mpt::winstring(_T("\0"), 1));
+	::WritePrivateProfileSection(mpt::ToWin(section).c_str(), keyvals.c_str(), filename.AsNative().c_str());
+}
+
+void CachedBatchedWindowsIniFileSettingsBackend::WriteRemovedSections(const std::set<mpt::ustring> &removeSections)
+{
+	OPENMPT_PROFILE_FUNCTION(Profiler::Settings);
+	for(const auto &section : removeSections)
+	{
+		const auto it = cache.find(section);
+		if(it != cache.end())
+		{
+			it->second = std::nullopt;
+		}
+	}
+	for(const auto &[section, keyvalues] : cache)
+	{
+		if(!keyvalues.has_value())
+		{
+			RemoveSectionRaw(section);
+		}
+	}
+	for(const auto &section : removeSections)
+	{
+		cache.erase(section);
+	}
+}
+
+void CachedBatchedWindowsIniFileSettingsBackend::WriteMultipleSettings(const std::map<SettingPath, std::optional<SettingValue>> &settings)
+{
+	OPENMPT_PROFILE_FUNCTION(Profiler::Settings);
+	std::map<mpt::ustring, std::map<SettingPath, std::optional<SettingValue>>> sectionssettings;
+	for(const auto &[path, value] : settings)
+	{
+		sectionssettings[path.GetRefSection()][path] = value;
+	}
+	for(const auto &[section, sectionsettings] : sectionssettings)
+	{
+		if(!cache[section].has_value())
+		{
+			cache[section].emplace();
+		}
+		std::map<mpt::ustring, std::optional<mpt::ustring>> &workingsectionsettings = cache[section].value();
+		for(const auto &[path, value] : sectionsettings)
+		{
+			if(value.has_value())
+			{
+				workingsectionsettings[path.GetRefKey()] = FormatValueAsIni(value.value());
+			} else
+			{
+				workingsectionsettings.erase(path.GetRefKey());
+			}
+		}
+		WriteSectionRaw(section, workingsectionsettings);
+	}
+}
+
+
+
+IniFileSettingsContainer::IniFileSettingsContainer(mpt::PathString filename)
+	: ImmediateWindowsIniFileSettingsBackend(std::move(filename))
+	, SettingsContainer(static_cast<ISettingsBackendFlavour<SettingsBatching::Single>*>(this))
+{
 	return;
 }
 
+BatchedIniFileSettingsContainer::BatchedIniFileSettingsContainer(mpt::PathString filename)
+	: CachedBatchedWindowsIniFileSettingsBackend(std::move(filename))
+	, SettingsContainer(static_cast<ISettingsBackendFlavour<SettingsBatching::Section>*>(this))
+{
+	return;
+}
+
 IniFileSettingsContainer::~IniFileSettingsContainer()
 {
 	return;
 }
 
+BatchedIniFileSettingsContainer::~BatchedIniFileSettingsContainer()
+{
+	return;
+}
 
 
+
 OPENMPT_NAMESPACE_END
Index: mptrack/SettingsIni.h
===================================================================
--- mptrack/SettingsIni.h	(revision 24593)
+++ mptrack/SettingsIni.h	(working copy)
@@ -13,12 +13,12 @@
 
 #include "openmpt/all/BuildSettings.hpp"
 
-
 #include "Settings.h"
 
 #include "mpt/io_file_atomic/atomic_file.hpp"
 
 #include <map>
+#include <optional>
 #include <set>
 #include <vector>
 
@@ -25,49 +25,112 @@
 
 OPENMPT_NAMESPACE_BEGIN
 
+
+// Version 1: ANSI
+// Version 2: UTF-16LE with BOM
+// Version 3: UTF8 with BOM (not supported by Windows)
+// Version 4: UTF8 without BOM and explicit version
+
+
+class IniFileHelpers
+{
+protected:
+	static mpt::ustring FormatValueAsIni(const SettingValue &value);
+	static SettingValue ParseValueFromIni(const mpt::ustring &str, const SettingValue &def);
+	static mpt::winstring GetSection(const SettingPath &path);
+	static mpt::winstring GetKey(const SettingPath &path);
+};
+
+
+class WindowsIniFileBase
+{
+protected:
+	const mpt::PathString filename;
+	mpt::IO::atomic_shared_file_ref file;
+protected:
+	WindowsIniFileBase(mpt::PathString filename);
+	~WindowsIniFileBase() = default;
+public:
+	void ConvertToUnicode(const mpt::ustring &backupTag = mpt::ustring());
+public:
+	const mpt::PathString &Filename() const;
+	mpt::PathString GetFilename() const;
+};
+
+
 class ImmediateWindowsIniFileSettingsBackend
 	: virtual public ISettingsBackend
 	, public ISettingsBackendFlavour<SettingsBatching::Single>
+	, public WindowsIniFileBase
+	, protected IniFileHelpers
 {
 private:
-	const mpt::PathString filename;
-	mpt::IO::atomic_shared_file_ref file;
-private:
 	std::vector<std::byte> ReadSettingRaw(const SettingPath &path, const std::vector<std::byte> &def) const;
 	mpt::ustring ReadSettingRaw(const SettingPath &path, const mpt::ustring &def) const;
 	double ReadSettingRaw(const SettingPath &path, double def) const;
 	int32 ReadSettingRaw(const SettingPath &path, int32 def) const;
 	bool ReadSettingRaw(const SettingPath &path, bool def) const;
+	void RemoveSectionRaw(const mpt::ustring &section);
+	void RemoveSettingRaw(const SettingPath &path);
 	void WriteSettingRaw(const SettingPath &path, const std::vector<std::byte> &val);
 	void WriteSettingRaw(const SettingPath &path, const mpt::ustring &val);
 	void WriteSettingRaw(const SettingPath &path, double val);
 	void WriteSettingRaw(const SettingPath &path, int32 val);
 	void WriteSettingRaw(const SettingPath &path, bool val);
-	void RemoveSettingRaw(const SettingPath &path);
-	void RemoveSectionRaw(const mpt::ustring &section);
-	static mpt::winstring GetSection(const SettingPath &path);
-	static mpt::winstring GetKey(const SettingPath &path);
 public:
-	ImmediateWindowsIniFileSettingsBackend(const mpt::PathString &filename);
+	ImmediateWindowsIniFileSettingsBackend(mpt::PathString filename);
 	~ImmediateWindowsIniFileSettingsBackend() override;
-	void ConvertToUnicode(const mpt::ustring &backupTag = mpt::ustring());
+public:
 	virtual SettingValue ReadSetting(const SettingPath &path, const SettingValue &def) const override;
+	virtual void RemoveSection(const mpt::ustring &section) override;
+	virtual void RemoveSetting(const SettingPath &path) override;
 	virtual void WriteSetting(const SettingPath &path, const SettingValue &val) override;
-	virtual void RemoveSetting(const SettingPath &path) override;
-	virtual void RemoveSection(const mpt::ustring &section) override;
-	const mpt::PathString& GetFilename() const { return filename; }
 };
 
-#ifndef IniFileSettingsBackend
+
+class CachedBatchedWindowsIniFileSettingsBackend
+	: virtual public ISettingsBackend
+	, public ISettingsBackendFlavour<SettingsBatching::Section>
+	, public WindowsIniFileBase
+	, protected IniFileHelpers
+{
+private:
+	std::map<mpt::ustring, std::optional<std::map<mpt::ustring, std::optional<mpt::ustring>>>> cache;
+private:
+	std::set<mpt::ustring> ReadSectionNamesRaw() const;
+	std::map<mpt::ustring, std::optional<mpt::ustring>> ReadNamedSectionRaw(const mpt::ustring &section) const;
+	std::map<mpt::ustring, std::optional<std::map<mpt::ustring, std::optional<mpt::ustring>>>> ReadAllSectionsRaw() const;
+	void RemoveSectionRaw(const mpt::ustring &section);
+	void WriteSectionRaw(const mpt::ustring &section, const std::map<mpt::ustring, std::optional<mpt::ustring>> &keyvalues);
+public:
+	CachedBatchedWindowsIniFileSettingsBackend(mpt::PathString filename);
+	~CachedBatchedWindowsIniFileSettingsBackend() override;
+public:
+	virtual void InvalidateCache() override;
+	virtual SettingValue ReadSetting(const SettingPath &path, const SettingValue &def) const override;
+	virtual void WriteRemovedSections(const std::set<mpt::ustring> &removeSections) override;
+	virtual void WriteMultipleSettings(const std::map<SettingPath, std::optional<SettingValue>> &settings) override;
+};
+
+
 #define IniFileSettingsBackend ImmediateWindowsIniFileSettingsBackend
-#endif
 
-class IniFileSettingsContainer : private IniFileSettingsBackend, public SettingsContainer
+
+class IniFileSettingsContainer : private ImmediateWindowsIniFileSettingsBackend, public SettingsContainer
 {
 public:
-	IniFileSettingsContainer(const mpt::PathString &filename);
+	IniFileSettingsContainer(mpt::PathString filename);
 	~IniFileSettingsContainer() override;
 };
 
 
+class BatchedIniFileSettingsContainer : private CachedBatchedWindowsIniFileSettingsBackend, public SettingsContainer
+{
+public:
+	BatchedIniFileSettingsContainer(mpt::PathString filename);
+	~BatchedIniFileSettingsContainer() override;
+};
+
+
+
 OPENMPT_NAMESPACE_END
Index: test/test.cpp
===================================================================
--- test/test.cpp	(revision 24593)
+++ test/test.cpp	(working copy)
@@ -2741,6 +2741,14 @@
 			VERIFY_EQUAL(inifile.ReadSetting(SettingPath{U_("S1"), U_("bar1")}, U_("empty")).as<mpt::ustring>(), U_("a"));
 			VERIFY_EQUAL(inifile.ReadSetting(SettingPath{U_("S1"), U_("bar2")}, U_("empty")).as<mpt::ustring>(), U_("empty"));
 		}
+		{
+			IniFileSettingsBackend inifile{filename};
+			const std::set<mpt::winstring> a = inifile.ReadSectionNamesRaw();
+			for(const mpt::winstring & s : a)
+			{
+				std::map<mpt::winstring, std::optional<mpt::winstring>> b = inifile.ReadNamedSectionRaw(s);
+			}
+		}
 		DeleteFile(mpt::support_long_path(filename.AsNative()).c_str());
 	}
 
@@ -2801,6 +2809,23 @@
 	}
 #endif
 
+	{
+		std::vector<std::byte> data;
+		for(std::size_t i = 0; i < 10; ++i)
+		{
+			data.push_back(mpt::byte_cast<std::byte>(static_cast<uint8>(i)));
+		}
+		{
+			IniFileSettingsBackend inifile{filename, CachePolicy::Cached};
+			inifile.WriteMultipleSettings({{SettingPath(U_("Test"), U_("Data")), data}});
+		}
+		{
+			IniFileSettingsBackend inifile{filename, CachePolicy::Cached};
+			VERIFY_EQUAL(inifile.ReadSetting(SettingPath(U_("Test"), U_("Data")), SettingValue(data)).as<std::vector<std::byte>>(), data);
+		}
+		DeleteFile(mpt::support_long_path(filename.AsNative()).c_str());
+	}
+
 #endif // MODPLUG_TRACKER
 
 }
