ตรวจสอบการใช้งานหน่วยความจำ

หากต้องการเพิ่มประสิทธิภาพการใช้หน่วยความจำของเกมอย่างมีประสิทธิภาพ คุณต้องทำความเข้าใจก่อนว่าแพลตฟอร์ม Android วัดหน่วยความจำอย่างไร และวิธีใช้การวัดและส่งข้อมูลทางไกลของระบบ, Diagnostic API และเครื่องมือสร้างโปรไฟล์ คู่มือนี้จะอธิบายรายละเอียดเกี่ยวกับวิธีตรวจสอบ บันทึก และวิเคราะห์การจัดสรรหน่วยความจำของเกมตามหลักเกณฑ์ใหม่ของแพลตฟอร์ม

ทำความเข้าใจเมตริก RSS และ Swap

หากต้องการวิเคราะห์และแก้ไขข้อบกพร่องเกี่ยวกับลักษณะการทำงานของหน่วยความจำในเกมอย่างมีประสิทธิภาพ คุณต้องทำความเข้าใจเมตริกทางเทคนิคที่แพลตฟอร์ม Android ใช้สำหรับการบังคับใช้หน่วยความจำอย่างละเอียด ดูข้อมูลพื้นฐานโดยละเอียดเกี่ยวกับวิธีประมวลผลและตรวจสอบพารามิเตอร์การวัดและส่งข้อมูลทางไกลนี้ ในสภาพแวดล้อมจริงได้ที่ เอกสารประกอบ Android Vitals - การใช้งานหน่วยความจำ (RSS แบบไม่ระบุตัวตน + Swap)

1. RSS แบบไม่ระบุตัวตน (RssAnon)

ขนาดชุดหน่วยความจำที่ใช้งาน (Resident Set Size หรือ RSS) จะวัดส่วนของหน่วยความจำที่กระบวนการใช้และเก็บไว้ใน RAM จริงของอุปกรณ์ RSS แบ่งออกเป็นหน่วยความจำที่สำรองด้วยไฟล์และหน่วยความจำแบบไม่ระบุตัวตน เมตริกการบังคับใช้ของ Android จะเน้นที่ RSS แบบไม่ระบุตัวตนเท่านั้น

  • สิ่งที่รวมอยู่: หน้าหน่วยความจำที่กระบวนการของเกมจัดสรรโดยตรง ซึ่งไม่ได้ลิงก์กับไฟล์จริงในพื้นที่เก็บข้อมูล หน้าเหล่านี้รวมถึงฮีป Java หรือ Kotlin, สแต็กการดำเนินการของเธรด และที่สำคัญคือการจัดสรรหน่วยความจำเนทีฟ (เช่น ตัวจัดสรรเครื่องมือ C++ ที่กำหนดเอง หรือบล็อกหน่วยความจำที่ขอโดยใช้ malloc หรือ new แบบเนทีฟ และมีการเปลี่ยนแปลงโดยตรรกะของเกม) ดูข้อมูลเพิ่มเติม เกี่ยวกับเมตริกนี้ได้ในพจนานุกรมหน่วยความจำของกระบวนการ (RSS)
  • ความสำคัญ: เครื่องมือเกมใช้พูลหน่วยความจำเนทีฟขนาดใหญ่เพื่อจัดการ ฟิสิกส์ การแสดงผล และตรรกะ เนื่องจากพูลเหล่านี้ไม่ได้สำรองด้วยไฟล์ จึงอยู่ใน RSS แบบไม่ระบุตัวตนทั้งหมดและเป็นส่วนใหญ่ของการใช้หน่วยความจำจริงของเกม

2. Swap ที่ไม่ได้บีบอัด (VmSwap)

Android ไม่รองรับพื้นที่ Swap แบบดั้งเดิมที่ใช้ดิสก์เนื่องจากข้อจำกัดด้านการสึกหรอของพื้นที่เก็บข้อมูลแฟลชและความหน่วง แต่จะใช้ zRAM (Swap ที่ไม่ได้บีบอัด) แทน

  • สิ่งที่รวมอยู่: เมื่อการใช้ RAM จริงเพิ่มขึ้น Daemon การจัดการหน่วยความจำของเคอร์เนลจะบีบอัดหน้าแบบไม่ระบุตัวตนที่ไม่ได้ใช้งานและย้ายไปยัง ส่วนที่ไม่ได้บีบอัดโดยเฉพาะของ RAM จริง (zRAM)
  • การคำนวณเมตริก: ระบบจะติดตามข้อมูลนี้ตามขนาดที่ไม่ได้บีบอัด (VmSwap) เพื่อประเมินความต้องการหน่วยความจำจริงของเกม หากเกมจัดสรรหน่วยความจำและระบบ Swap หน่วยความจำนั้นไปยัง zRAM ระบบจะยังนับหน่วยความจำนั้นรวมอยู่ในการใช้หน่วยความจำทั้งหมดของเกม

3. สถานะของกระบวนการ

การใช้งานหน่วยความจำจะแบ่งตามสถานะของกระบวนการใน Android Vitals สำหรับนักพัฒนาเกม SDK หรือเกมของบุคคลที่สามอาจทริกเกอร์บริการที่ผู้ใช้รับรู้หรือบริการเบื้องหลังโดยไม่คาดคิด

  • สิ่งที่รวมอยู่: เบื้องหน้า, บริการที่ผู้ใช้รับรู้, เบื้องหลัง และ แคช
  • ความสำคัญ: สถานะของกระบวนการที่แตกต่างกันจะส่งผลต่อการจัดการหน่วยความจำของระบบปฏิบัติการ Android ที่แตกต่างกัน คุณอาจไม่ทราบว่าเกมกำลังทำงานด้วยสถานะของกระบวนการที่ละเอียดอ่อนหาก SDK ของบุคคลที่สามทริกเกอร์งานเบื้องหลังโดยไม่ได้ตั้งใจ ตรวจสอบว่าเกมกำลังทำงานอยู่เบื้องหลังหรือไม่ โดยใช้ RunningAppProcessInfo

Application Programming Interface (API)

Android มี System API ที่ช่วยให้เกมตอบสนองต่อการใช้หน่วยความจำมากเกินไปแบบไดนามิกและบันทึกการวินิจฉัยหน่วยความจำโดยละเอียดในขณะรันไทม์

ตอบสนองต่อเหตุการณ์การลดหน่วยความจำ

ระบบใช้ onTrimMemory เพื่อแจ้งให้แอปทราบเกี่ยวกับเหตุการณ์วงจรชีวิตซึ่งเป็นโอกาสที่ดีที่แอปจะลดการใช้งานหน่วยความจำโดยสมัครใจ และหลีกเลี่ยงไม่ให้ระบบหยุดแอปเนื่องจากหน่วยความจำไม่เพียงพอ (Low-Memory Killer หรือ LMK) เพื่อปล่อยหน่วยความจำให้ แอปอื่นๆ ใช้งาน

หากระบบหยุดแอปที่ทำงานอยู่เบื้องหลัง ผู้ใช้จะพบว่าการเริ่มแอปแบบ Cold Start ช้าเมื่อกลับมาใช้งานต่อ การลดการใช้งานหน่วยความจำเบื้องหลังจะช่วยป้องกันการหยุดแอปที่ทำงานอยู่เบื้องหลัง

เมื่อตอบสนองต่อเหตุการณ์การลดหน่วยความจำ ให้ปล่อยการจัดสรรหน่วยความจำขนาดใหญ่ที่สร้างใหม่ได้และไม่จำเป็นต้องใช้ในทันที

  • ตัวอย่าง: ลดหรือล้างบิตแมปที่แคชไว้ (ถอดรหัสจากพื้นที่เก็บข้อมูลในเครื่อง) เพื่อตอบสนองต่อ TRIM_MEMORY_UI_HIDDEN

Kotlin

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {
    override fun onTrimMemory(level: Int) {
        if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // Release memory related to UI elements, such as bitmap caches.
        }
        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            // Release memory related to background processing, such as by
            // closing a database connection.
        }
    }
}

Java

public class MainActivity extends AppCompatActivity implements ComponentCallbacks2 {
    public void onTrimMemory(int level) {
        switch (level) {
            if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
                // Release memory related to UI elements, such as bitmap caches.
            }
            if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
                // Release memory related to background processing, such as by
                // closing a database connection.
            }
        }
    }
}

ProfilingManager

เปิดตัวใน Android 15 (ระดับ API 35) ProfilingManager API ช่วยให้แอปพลิเคชันบันทึกสแนปช็อตที่กำหนดไว้แบบเป็นโปรแกรม (เช่น โปรไฟล์ฮีป, การติดตามระบบ และฮีปดัมป์ Java) ได้โดยตรงในขณะรันไทม์

นักพัฒนาแอปสามารถทริกเกอร์การบันทึกด้วยตนเองในฉากที่เฉพาะเจาะจง หรือลงทะเบียนทริกเกอร์อัตโนมัติ เช่น TRIGGER_TYPE_ANOMALY เพื่อทริกเกอร์การบันทึกโดยอัตโนมัติเมื่อกระบวนการของเกมละเมิดเกณฑ์ของตัวจำกัดหน่วยความจำ อย่างไรก็ตาม นักพัฒนาเกมต้องคำนึงถึงข้อจำกัดที่สำคัญในเครื่องมือเกมสมัยใหม่

หมายเหตุ: เครื่องมือเกมสมัยใหม่ (เช่น Unity หรือ Unreal) จะจัดการประสิทธิภาพการดำเนินการโดยการจัดสรรบล็อกหน่วยความจำเสมือนขนาดใหญ่ล่วงหน้าจากเคอร์เนลโดยใช้ mmap ที่มีแฟล็ก MAP_ANONYMOUS จากนั้นเครื่องมือจะใช้ตัวจัดสรรย่อยที่กำหนดเอง (เช่น ตัวจัดการหน่วยความจำเนทีฟของ Unity หรือ BinnedAllocators ของ Unreal) เพื่อแบ่งและจัดสรรบล็อกหน่วยความจำภายใน

ApplicationExitInfo

หากระบบหยุดเกมที่ทำงานอยู่เบื้องหลังหรือหยุดเกมเนื่องจากละเมิดขีดจำกัดหน่วยความจำของกระบวนการแต่ละรายการ กลไก Crash Dump แบบเนทีฟหรือ Java มาตรฐาน (เช่น Firebase Crashlytics) จะไม่ลงทะเบียนเหตุการณ์ นักพัฒนาแอปควรใช้ประโยชน์จาก ApplicationExitInfo API เมื่อเริ่มเกมเพื่อค้นหาและบันทึกการหยุดเหล่านี้ แบบเป็นโปรแกรม

  • การใช้งาน: เมื่อเริ่มต้น ให้เรียก ActivityManager.getHistoricalProcessExitReasons() เพื่อดึงข้อมูลเหตุผลที่ออกจากระบบของเซสชันล่าสุด
  • เหตุผลหลักที่ออกจากระบบเนื่องจากหน่วยความจำ
    • REASON_LOW_MEMORY: บ่งชี้ว่าระบบหยุดกระบวนการเนื่องจากหน่วยความจำไม่เพียงพอ (Low Memory Killer หรือ LMK) การหยุดนี้เกิดขึ้นเมื่อการใช้หน่วยความจำมากเกินไปในอุปกรณ์สูงและระบบปฏิบัติการต้องเรียกคืน RAM เหตุผลที่ออกจากระบบนี้บ่งชี้ว่าการใช้หน่วยความจำเบื้องหลังของเกมมีขนาดใหญ่เกินไปจนไม่สามารถทำงานร่วมกับแอปพลิเคชันอื่นๆ ได้
    • REASON_MEMORY_LIMITER (Android 17 (ระดับ API 37) ขึ้นไป): บ่งชี้ว่าระบบหยุดกระบวนการโดยเฉพาะเนื่องจากกระบวนการใช้หน่วยความจำเกินขีดจำกัดหน่วยความจำ cgroup (RssAnon + VmSwap) ที่ตัวจำกัดหน่วยความจำของแพลตฟอร์มกำหนด การหยุดนี้อาจเกิดขึ้นได้แม้ว่าอุปกรณ์จะมีหน่วยความจำจริงเหลืออยู่มาก ซึ่งแสดงถึงการละเมิดขีดจำกัดของกระบวนการแต่ละรายการโดยตรง

ใช้เครื่องมือที่พร้อมใช้งาน

ใช้เครื่องมือแพลตฟอร์มต่อไปนี้ระหว่างการพัฒนาและการประกันคุณภาพเพื่อวัดการใช้งานหน่วยความจำของเกมอย่างแม่นยำ

meminfo

เครื่องมือนี้จะรวบรวมสถิติหน่วยความจำเพื่อแสดงจำนวนหน่วยความจำ PSS ที่ จัดสรรและหมวดหมู่ที่ใช้

พิมพ์สถิติ meminfo ด้วยวิธีใดวิธีหนึ่งต่อไปนี้

  • ใช้คำสั่ง adb shell dumpsys meminfo package-name
  • ใช้การเรียก MemoryInfo จาก Android Debug API

สถิติ PrivateDirty จะแสดงจำนวน RAM ภายในกระบวนการ ที่ย้ายไปยังดิสก์ไม่ได้และไม่ได้แชร์กับกระบวนการอื่นๆ ระบบจะใช้จำนวนนี้ได้ส่วนใหญ่เมื่อหยุดกระบวนการนั้น

จุดติดตามหน่วยความจำ

จุดติดตามหน่วยความจำจะติดตามจำนวนหน่วยความจำ RSS ที่เกมใช้ การคำนวณการใช้งานหน่วยความจำ RSS เร็วกว่าการคำนวณการใช้งาน PSS มาก เนื่องจากคำนวณได้เร็วกว่า RSS จึงแสดงการเปลี่ยนแปลงขนาดหน่วยความจำได้ละเอียดกว่าเพื่อการวัดการใช้งานหน่วยความจำสูงสุดที่แม่นยำยิ่งขึ้น ดังนั้น คุณจึงสังเกตเห็นการใช้งานหน่วยความจำสูงสุดที่อาจทำให้หน่วยความจำของเกมเต็มได้ง่ายขึ้น

Perfetto

Perfetto เป็นชุดเครื่องมือสำหรับการรวบรวมข้อมูลประสิทธิภาพและหน่วยความจำ ในอุปกรณ์ รวมถึงแสดงข้อมูลใน UI บนเว็บ เครื่องมือนี้รองรับการติดตามที่ยาวนานตามต้องการ คุณจึงดูการเปลี่ยนแปลง RSS เมื่อเวลาผ่านไปได้ นอกจากนี้ คุณยังเรียกใช้การค้นหา SQL ในข้อมูลที่เครื่องมือสร้างขึ้นเพื่อการประมวลผลแบบออฟไลน์ได้ด้วย เปิดใช้การติดตามแบบยาว จากแอปการติดตามระบบ ตรวจสอบว่าได้เปิดใช้หมวดหมู่memory:Memory สำหรับการติดตามแล้ว สำหรับการเพิ่มแท็กหน่วยความจำที่กำหนดเองในการพัฒนาและ การทดสอบ คุณยังใช้ heapprofd API (เบต้า) ได้ด้วย

ตรวจสอบ RssAnon และ Swap ใน Perfetto

หากต้องการตรวจสอบหน่วยความจำแบบไม่ระบุตัวตนและผลกระทบของ zRAM Swap ในเกม ให้โหลดไฟล์การติดตาม ใน UI บนเว็บที่ ui.perfetto.dev แล้วทำตามเทคนิคการวิเคราะห์ ต่อไปนี้ ซึ่งออกแบบมาสำหรับการศึกษาเชิงลึกเกี่ยวกับหน่วยความจำ (ดูรายละเอียดเพิ่มเติมได้ที่ การศึกษาเชิงลึกเกี่ยวกับการวิเคราะห์หน่วยความจำ Perfetto)

1. การแสดงภาพตัวนับหน่วยความจำในไทม์ไลน์

  • ค้นหากระบวนการ: ในรายการการนำทาง ให้ค้นหาชื่อแพ็กเกจหรือชื่อกระบวนการของเกม
  • ขยายกลุ่มแทร็ก: คลิกแถวกระบวนการเพื่อขยายแทร็กเธรด แล้วค้นหากลุ่มย่อยที่ชื่อว่าหน่วยความจำ
  • วิเคราะห์แทร็ก:
    • mem.rss.anon (RSS แบบไม่ระบุตัวตน): กราฟเส้นนี้แสดง RAM จริงแบบเรียลไทม์ ที่พูลหน่วยความจำที่ไม่ได้จัดการของเกมใช้ ตรวจสอบไทม์ไลน์นี้ระหว่างการโหลดฉาก ป๊อปอัป UI หรือการเปลี่ยนผ่านของเกมเพลย์เพื่อตรวจสอบการใช้งานหน่วยความจำสูงสุด
    • mem.swap (Swap ที่บีบอัดหรือ VmSwap): กราฟนี้จะพล็อต ขนาดของบล็อกหน่วยความจำก่อนบีบอัดที่ย้ายไปยัง zRAM กิจกรรม Swap สูงที่เกิดขึ้นพร้อมกับเกมเพลย์บ่งชี้ว่าเกมกำลังทำงานในอุปกรณ์ที่มีหน่วยความจำจำกัดและระบบกำลังบีบอัดชิ้นงานเบื้องหลังอย่างต่อเนื่อง

2. การเรียกใช้การค้นหา SQL (ตัวประมวลผลการติดตาม) หากต้องการวิเคราะห์แบบออฟไลน์โดยละเอียด คุณ สามารถเรียกใช้การค้นหา SQL ได้โดยตรงภายในคอนโซล UI ของ Perfetto หรือใช้ ไลบรารี Trace Processor Python แบบสแตนด์อโลนเพื่อคำนวณการใช้งานหน่วยความจำสูงสุดทางสถิติ

  • ค้นหาการจัดสรร RSS แบบไม่ระบุตัวตนสูงสุด

    SELECT
      max(value) / 1024 / 1024 AS max_rss_anon_mb
    FROM counter
    JOIN counter_track ON counter.track_id = counter_track.id
    WHERE counter_track.name = 'mem.rss.anon'
      AND counter_track.upid IN (
        SELECT upid FROM process WHERE name = 'your.game.package.name'
      );
    
  • เชื่อมโยง RssAnon และ VmSwap ที่การประทับเวลาที่กำหนด

    SELECT
      ts,
      track.name AS metric_type,
      value / 1024 / 1024 AS size_mb
    FROM counter
    JOIN counter_track track ON counter.track_id = track.id
    WHERE (track.name = 'mem.rss.anon' OR track.name = 'mem.swap')
      AND track.upid IN (
        SELECT upid FROM process WHERE name = 'your.game.package.name'
      )
    ORDER BY ts ASC;
    

ดูรายละเอียดเพิ่มเติมเกี่ยวกับการตรวจสอบไฟล์การติดตามโดยใช้ Android Studio ได้ที่ ตรวจสอบ การติดตามระบบ: หน่วยความจำของกระบวนการ (RSS) ดูรายละเอียดเกี่ยวกับการเขียนสคริปต์โปรไฟล์หน่วยความจำ ได้ที่ บันทึกการจัดสรรแบบเนทีฟ

heapprofd

heapprofd เป็นเครื่องมือติดตามหน่วยความจำซึ่งเป็นส่วนหนึ่งของ Perfetto เครื่องมือนี้ช่วยคุณค้นหาหน่วยความจำรั่วได้โดยแสดงตำแหน่งที่จัดสรรหน่วยความจำโดยใช้ malloc คุณสามารถเริ่ม heapprofd ได้โดยใช้สคริปต์ Python และเนื่องจากเครื่องมือนี้มีค่าใช้จ่ายต่ำ จึงไม่ส่งผลต่อประสิทธิภาพการทำงานเหมือนเครื่องมืออื่นๆ เช่น Malloc Debug

bugreport

bugreport เป็นเครื่องมือบันทึกเพื่อดูว่าเกมขัดข้องเนื่องจากหน่วยความจำเต็มหรือไม่ เอาต์พุตของเครื่องมือนี้มีรายละเอียดมากกว่าการใช้ logcat มาก เครื่องมือนี้มีประโยชน์สำหรับการแก้ไขข้อบกพร่องเกี่ยวกับหน่วยความจำ เนื่องจากจะแสดงว่าเกมขัดข้องเนื่องจากหน่วยความจำเต็มหรือระบบหยุดเกมเนื่องจากหน่วยความจำไม่เพียงพอ (LMK)

ดูข้อมูลเพิ่มเติมได้ที่ บันทึกและอ่านรายงานข้อบกพร่อง

เครื่องมือเกมเอนจิน

แม้ว่าบันทึกระดับแพลตฟอร์มและการวัดและส่งข้อมูลทางไกลของระบบจะมีความสำคัญอย่างยิ่งต่อการติดตามเกณฑ์และการปฏิบัติตามข้อกำหนดของระบบปฏิบัติการ แต่เครื่องมือเฉพาะของเครื่องมือเกมจะช่วยให้คุณระบุการจัดสรรกลับไปยังออบเจ็กต์ของเกม ลักษณะการทำงานของสคริปต์ และลำดับชั้นของฉากที่ใช้งานอยู่ได้โดยตรง

Unity

ในสภาพแวดล้อม Unity Engine คุณสามารถประมาณการใช้หน่วยความจำที่ใช้ RSS แบบไม่ระบุตัวตน + Swap ของ Android ได้อย่างแม่นยำในขณะรันไทม์ด้วยความน่าเชื่อถือสูง (โดยทั่วไปจะแสดงความแปรปรวนน้อยกว่า 10% เมื่อเทียบกับค่าระดับระบบปฏิบัติการจริง) โดยใช้เครื่องมือและคลาสการสร้างโปรไฟล์แบบเนทีฟของ Unity

ดูบทแนะนำแบบทีละขั้นตอนที่สมบูรณ์ ซึ่งรวมถึงกฎการกำหนดค่าและสคริปต์รันไทม์ ได้ที่วิธีตรวจสอบหน่วยความจำด้วยเครื่องมือ Unity

  • Unity Profiler API: คุณสามารถประมาณการใช้หน่วยความจำที่ไม่ได้จัดการของเกมแบบเป็นโปรแกรมในขณะรันไทม์ได้โดยการค้นหาเมตริกหลักของเครื่องมือ ดังนี้
    • การใช้คลาส Profiler: ติดตามการจัดสรรหน่วยความจำทั้งหมดโดยการรวม ค่า Profiler.GetTotalReservedMemoryLong() และ Profiler.GetMonoHeapSizeLong()
    • การใช้คลาส ProfilerRecorder: ตรวจสอบหมวดหมู่หน่วยความจำ แบบไดนามิก หากต้องการสร้างค่าประมาณพื้นฐานที่เชื่อถือได้ ให้ดึงข้อมูลหน่วยความจำที่สงวนไว้ทั้งหมด (ในบิลด์เวอร์ชันที่เผยแพร่) หรือลบหน่วยความจำที่สงวนไว้ของ Gfx ออกจากหน่วยความจำที่สงวนไว้ทั้งหมด (ในบิลด์เวอร์ชันที่พัฒนา) เพื่อนำคอมโพเนนต์หน่วยความจำกราฟิกที่สำรองด้วยไฟล์ออก
  • Unity Memory Profiler: หากต้องการระบุและแก้ไขข้อบกพร่องเกี่ยวกับหน่วยความจำรั่วแบบออฟไลน์ ให้บันทึกสแนปช็อตหน่วยความจำและตรวจสอบแผนภูมิหน่วยความจำที่ใช้งานในอุปกรณ์ ที่อยู่ในส่วนหน่วยความจำทั้งหมด หากต้องการคำนวณการใช้หน่วยความจำโดยประมาณ ให้รวมค่าทั้งหมดของหมวดหมู่ต่อไปนี้ ไม่ได้ติดตาม, Android Runtime, เนทีฟ และมีการจัดการ
    • ข้อจำกัดของ zRAM: ในสภาวะที่หน่วยความจำจำกัด เคอร์เนลของ Android จะบีบอัดหน้าหน่วยความจำที่ไม่ได้ใช้งานลงในพื้นที่ Swap (zRAM) เนื่องจาก Unity Memory Profiler ตรวจหาพารามิเตอร์ Swap ระดับระบบปฏิบัติการไม่ได้ คุณจึงอาจเห็นความคลาดเคลื่อนเล็กน้อยในการใช้หน่วยความจำในฉากที่ใช้หน่วยความจำมาก ตรวจสอบเพื่อเปรียบเทียบค่าประมาณกับ Perfetto เพื่อยืนยันค่าที่แน่นอน