AssetBundle 은 압축 방식에 따라 여는 비용이 다르다
번들 파일을 작게 만들려고 기본값인 LZMA 로 빌드했더니, 번들을 여는 순간 프레임이 튀고 메모리가 번들 크기보다 더 늘어납니다. 같은 번들을 다른 API 로 열면 또 다른 숫자가 나옵니다.
AssetBundle 을 여는 비용은 압축 방식과 여는 API 의 조합으로 정해집니다. LZMA 는 통째로 풀어야 하고, LZ4 와 무압축은 필요한 부분만 읽습니다. 그리고 API 마다 원본을 어디에 두는지가 다릅니다.
같은 2048×2048 텍스처 하나(GPU 에 올리기 전 12 MiB)를 세 압축으로 빌드해서 쟀습니다.
LZMA 는 열 때 통째로 푼다
LZMA 는 번들 전체가 하나의 압축 흐름입니다. 에셋 하나를 읽으려 해도 앞에서부터 다 풀어야 합니다. 그래서 매뉴얼은 LZMA 번들을 LoadFromFile 로 열면 항상 메모리 캐시를 쓴다, 즉 풀어 놓은 사본을 메모리에 둔다고 적습니다. LZ4 는 덩어리 단위로 나눠 압축해서 필요한 덩어리만 풉니다.
LZMA 는 열기만 했는데 무압축 크기만큼 메모리가 늘고 277 ms 가 걸렸습니다. 한 프레임 예산의 열 배가 넘습니다. LZ4 와 무압축은 파일 크기와 상관없이 1 ms 안쪽이었고, 실제 텍스처 데이터는 LoadAsset 때 읽혔습니다. LoadAsset 시간은 세 경우 모두 7 ms 안팎으로 비슷했습니다.
LZMA 는 받을 때 작은 대신 열 때 비쌉니다. 디스크에서 바로 여는 번들은 LZ4 로 빌드합니다(BuildAssetBundleOptions.ChunkBasedCompression).
LoadFromMemory 는 배열과 사본을 같이 든다
복호화한 번들처럼 바이트 배열로 가진 것은 AssetBundle.LoadFromMemory 로 엽니다. Unity 의 가이드는 이 API 의 최대 메모리가 번들 크기의 최소 두 배 라고 적습니다. 넘긴 관리 배열과, API 가 만든 네이티브 사본이 동시에 있기 때문입니다. LZ4 번들(11.38 MiB)로 단계마다 쟀습니다.
배열을 놓아도 네이티브 사본은 번들을 내릴 때까지 남습니다. 같은 파일을 LoadFromFile 로 열었을 때 연 직후 native 증가가 +0.26 MiB 였던 것과 비교하면, LZ4 번들인데도 번들 전체를 상주시킨 셈입니다.
UnityWebRequest 는 버전을 줘야 디스크에 둔다
서버에서 받는 번들은 UnityWebRequestAssetBundle.GetAssetBundle 로 받습니다. 매뉴얼에 따르면 버전(또는 해시)을 넘기면 디스크 캐시에 저장하고, 넘기지 않으면 메모리 캐시를 씁니다. 디스크 캐시에 넣을 때는 LZMA 를 LZ4 로 다시 써서, 첫 로드만 느리고 그 뒤로는 빠르다고 합니다. 로컬 HTTP 서버에서 LZMA 번들을 받았습니다.
| 경우 | 받는 시간 | 연 직후 native | 같은 프로세스 2회차 |
|---|---|---|---|
| 버전 없음 | 325.8 ms | +14.49 MiB | 303.7 ms, 다시 받음 |
| 버전 있음, 캐시 비움 | 323.1 ms | +2.47 MiB | 2.67 ms, 0 B |
| 버전 있음, 캐시 있음 | 26.3 ms, 0 B | +2.18 MiB | 3.09 ms |
// 버전 없이: 매번 받고, 풀어 놓은 사본이 메모리에 있다
UnityWebRequestAssetBundle.GetAssetBundle(url);
// 버전을 주면: 디스크 캐시에 두고 다음부터 받지 않는다
UnityWebRequestAssetBundle.GetAssetBundle(url, version: 1, crc: 0);
버전이 없으면 같은 프로세스에서 두 번째로 불러도 다시 받았습니다. 버전을 주면 메모리 증가가 약 5분의 1 에서 7분의 1 로 줄었고, 캐시가 찬 뒤에는 받지 않고 3 ms 안팎에 열렸습니다. 매뉴얼은 차이를 “적어도 10배” 라고 적지만 이 번들에서는 그만큼은 아니었습니다.
직접 확인하는 테스트 코드
같은 에셋을 세 압축으로 빌드하고 LoadFromFile 직후의 메모리와 시간을 찍습니다. 메모리는 Profiler.GetTotalAllocatedMemoryLong 의 증가분입니다. Development Build 에서 돌립니다.
// 에디터: 압축만 다르게 세 번 빌드한다
foreach (var (name, opt) in new[]
{
("lzma", BuildAssetBundleOptions.None),
("lz4", BuildAssetBundleOptions.ChunkBasedCompression),
("none", BuildAssetBundleOptions.UncompressedAssetBundle),
})
{
var builds = new[] { new AssetBundleBuild { assetBundleName = "payload", assetNames = new[] { "Assets/Payload.png" } } };
Directory.CreateDirectory("Builds/Bundles/" + name);
BuildPipeline.BuildAssetBundles("Builds/Bundles/" + name, builds,
opt | BuildAssetBundleOptions.ForceRebuildAssetBundle, BuildTarget.StandaloneWindows64);
}
// Player: 여는 순간의 시간과 네이티브 메모리
IEnumerator Open(string path)
{
GC.Collect();
yield return null;
long before = Profiler.GetTotalAllocatedMemoryLong();
var sw = Stopwatch.StartNew();
AssetBundle bundle = AssetBundle.LoadFromFile(path);
sw.Stop();
long after = Profiler.GetTotalAllocatedMemoryLong();
Debug.Log($"{path}: {sw.Elapsed.TotalMilliseconds:F2} ms, native +{(after - before) / 1048576.0:F2} MiB");
bundle.Unload(true);
}
LZMA 만 수백 ms 와 압축 전 크기만큼의 증가가 나오면 같은 결과입니다. 압축별로 새 프로세스에서 재야 앞 실행의 캐시가 섞이지 않습니다.
주의할 점
1. 디스크에서 바로 여는 번들을 LZMA 로 두지 않는다
안 지키면 생기는 문제. LoadFromFile 이 번들 전체를 풀어 메모리에 두고, 여는 프레임이 수백 ms 멈춥니다. 번들이 클수록 둘 다 커집니다.
이렇게 쓴다. StreamingAssets 처럼 디스크에서 여는 번들은 LZ4(ChunkBasedCompression)로 빌드합니다. 받는 크기를 줄이고 싶어 LZMA 로 배포했다면 받을 때 버전을 줘서 디스크 캐시에 LZ4 로 다시 쓰게 합니다.
2. LoadFromMemory 를 기본 경로로 쓰지 않는다
안 지키면 생기는 문제. 여는 순간 배열과 사본으로 번들 크기의 두 배를 쓰고, 배열을 놓아도 번들 전체 크기의 사본이 번들을 내릴 때까지 남습니다. 번들을 여러 개 열어 두는 게임에서는 그대로 쌓입니다.
이렇게 쓴다. 파일로 있는 번들은 LoadFromFile 로 엽니다. 암호화처럼 꼭 메모리에서 열어야 하면 연 직후 배열을 놓고, 그 번들은 쓰는 동안만 열어 둡니다.
3. UnityWebRequest 로 받을 때는 버전이나 해시를 준다
안 지키면 생기는 문제. 버전 없이 받으면 디스크 캐시에 남지 않아 같은 번들을 매번 다시 받고, 풀어 놓은 사본이 메모리에 올라옵니다.
이렇게 쓴다. 번들 매니페스트의 해시나 직접 매긴 버전을 GetAssetBundle(url, version, crc) 에 넘깁니다. 번들이 바뀌면 그 값도 바뀌게 해서 새 번들만 받게 합니다.
정리
AssetBundle 을 여는 비용은 압축 방식과 여는 API 가 원본을 어디에 두느냐로 정해집니다.
- LZMA 를
LoadFromFile로 열면 전체를 풀어 두어 277 ms, +13.31 MiB 가 들었습니다. LZ4 와 무압축은 1 ms 안쪽입니다. LoadFromMemory는 배열과 네이티브 사본을 같이 들고, 사본은 번들을 내릴 때까지 남습니다.UnityWebRequest는 버전을 줘야 디스크 캐시를 쓰고, 이 번들에서는 메모리 증가가 5분의 1 에서 7분의 1 로 줄었습니다.