'System.OutOfMemoryException' ถูกโยนทิ้งไปเมื่อยังมีหน่วยความจำเหลืออยู่มากมาย


93

นี่คือรหัสของฉัน:

int size = 100000000;
double sizeInMegabytes = (size * 8.0) / 1024.0 / 1024.0; //762 mb
double[] randomNumbers = new double[size];

ข้อยกเว้น: ข้อยกเว้นประเภท 'System.OutOfMemoryException' ถูกโยนทิ้ง

ฉันมีหน่วยความจำ 4GB ในเครื่องนี้ 2.5GB ฟรีเมื่อฉันเริ่มการทำงานนี้มีพื้นที่ว่างเพียงพอบนพีซีอย่างชัดเจนที่จะจัดการกับตัวเลขสุ่ม 762mb ของ 100000000 ฉันต้องการจัดเก็บตัวเลขสุ่มให้ได้มากที่สุดเท่าที่จะเป็นไปได้ที่มีหน่วยความจำ เมื่อฉันไปผลิตจะมี 12GB บนกล่องและฉันต้องการใช้ประโยชน์จากมัน

CLR บังคับให้ฉันใช้หน่วยความจำสูงสุดเริ่มต้นหรือไม่ และฉันจะขอเพิ่มได้อย่างไร?

อัปเดต

ฉันคิดว่าการแบ่งสิ่งนี้ออกเป็นชิ้นเล็ก ๆ และการเพิ่มความต้องการหน่วยความจำของฉันทีละน้อยจะช่วยได้หากปัญหาเกิดจากการแยกส่วนหน่วยความจำแต่ฉันไม่สามารถผ่านขนาด ArrayList ทั้งหมดที่มีขนาด 256mb ไม่ว่าฉันจะปรับแต่ง blockSizeก็ตาม

private static IRandomGenerator rnd = new MersenneTwister();
private static IDistribution dist = new DiscreteNormalDistribution(1048576);
private static List<double> ndRandomNumbers = new List<double>();

private static void AddNDRandomNumbers(int numberOfRandomNumbers) {
    for (int i = 0; i < numberOfRandomNumbers; i++) {
      ndRandomNumbers.Add(dist.ICDF(rnd.nextUniform()));                
  }
}

จากวิธีการหลักของฉัน:

int blockSize = 1000000;

while (true) {
  try
  {
    AddNDRandomNumbers(blockSize);                    
  }
  catch (System.OutOfMemoryException ex)
  {
    break;
  }
}            
double arrayTotalSizeInMegabytes = (ndRandomNumbers.Count * 8.0) / 1024.0 / 1024.0;

6
ฉันขอแนะนำให้ปรับโครงสร้างแอปพลิเคชันของคุณใหม่เพื่อที่คุณจะได้ไม่ต้องใช้หน่วยความจำมากนัก คุณกำลังทำอะไรเพื่อต้องการตัวเลขร้อยล้านทั้งหมดในหน่วยความจำในครั้งเดียว?
Eric Lippert

2
คุณไม่ได้ปิดการใช้งาน pagefile หรืออะไรโง่ ๆ แบบนั้นใช่ไหม
jalf

@EricLippert ฉันพบปัญหานี้เมื่อทำงานกับปัญหา P เทียบกับ NP ( claymath.org/millenium-pro issues/ p-vs -np-problem ) คุณมีคำแนะนำในการลดการใช้หน่วยความจำในการทำงานหรือไม่? (เช่นการทำให้เป็นอนุกรมและการจัดเก็บข้อมูลบนฮาร์ดดิสก์โดยใช้ชนิดข้อมูล C ++ เป็นต้น)
devinbost

@bosit นี่คือเว็บไซต์คำถามและคำตอบ หากคุณมีคำถามทางเทคนิคเฉพาะเกี่ยวกับโค้ดจริงให้โพสต์เป็นคำถาม
Eric Lippert

@bostIT ลิงก์สำหรับปัญหา P เทียบกับ NP ในความคิดเห็นของคุณไม่ถูกต้องอีกต่อไป
RBT

คำตอบ:


142

คุณอาจต้องการอ่านสิ่งนี้: " " หน่วยความจำไม่เพียงพอ"ไม่อ้างถึงหน่วยความจำกายภาพ " โดย Eric Lippert

ในระยะสั้นและเรียบง่ายมาก "หน่วยความจำไม่เพียงพอ" ไม่ได้หมายความว่าจำนวนหน่วยความจำที่มีอยู่นั้นน้อยเกินไป สาเหตุที่พบบ่อยที่สุดคือภายในพื้นที่ที่อยู่ปัจจุบันไม่มีส่วนของหน่วยความจำที่ต่อเนื่องกันซึ่งมีขนาดใหญ่พอที่จะรองรับการจัดสรรที่ต้องการ หากคุณมี 100 บล็อกแต่ละบล็อกมีขนาดใหญ่ 4 MB นั่นจะไม่ช่วยคุณได้เมื่อคุณต้องการบล็อกขนาด 5 MB หนึ่งบล็อก

ประเด็นสำคัญ:

  • การจัดเก็บข้อมูลที่เราเรียกว่า“ความทรงจำ” กระบวนการที่อยู่ในความคิดของฉันมองเห็นที่ดีที่สุดเป็นไฟล์ขนาดใหญ่บนดิสก์
  • RAM สามารถมองเห็นได้ว่าเป็นเพียงการเพิ่มประสิทธิภาพเท่านั้น
  • จำนวนหน่วยความจำเสมือนทั้งหมดที่โปรแกรมของคุณใช้นั้นไม่ได้เกี่ยวข้องกับประสิทธิภาพอย่างมหาศาล
  • "RAM หมด" แทบจะไม่ส่งผลให้เกิดข้อผิดพลาด "หน่วยความจำไม่เพียงพอ" แทนที่จะเกิดข้อผิดพลาด แต่กลับส่งผลให้ประสิทธิภาพการทำงานแย่ลงเนื่องจากค่าใช้จ่ายทั้งหมดของการจัดเก็บข้อมูลจริงบนดิสก์กลายเป็นเรื่องที่เกี่ยวข้อง

"ถ้าคุณมี 100 บล็อกแต่ละบล็อกมีขนาดใหญ่ 4 MB นั่นจะไม่ช่วยคุณได้เมื่อคุณต้องการบล็อกขนาด 5 MB" - ฉันคิดว่ามันจะดีกว่าด้วยการแก้ไขเล็กน้อย: "ถ้าคุณมี" 100 ช่อง "บล็อก" .
OfirD

31

ตรวจสอบว่าคุณกำลังสร้างกระบวนการ 64 บิตไม่ใช่แบบ 32 บิตซึ่งเป็นโหมดการคอมไพล์เริ่มต้นของ Visual Studio ในการดำเนินการนี้ให้คลิกขวาที่โครงการของคุณ Properties -> Build -> platform target: x64 ในฐานะที่เป็นกระบวนการ 32 บิตแอปพลิเคชัน Visual Studio ที่คอมไพล์เป็น 32 บิตมีขีด จำกัด หน่วยความจำเสมือนที่ 2GB

กระบวนการ 64 บิตไม่มีข้อ จำกัด นี้เนื่องจากใช้ตัวชี้ 64 บิตดังนั้นพื้นที่แอดเดรสสูงสุดตามทฤษฎี (ขนาดของหน่วยความจำเสมือน) คือ 16 เอ็กซาไบต์ (2 ^ 64) ในความเป็นจริง Windows x64 จำกัด หน่วยความจำเสมือนของกระบวนการไว้ที่ 8TB วิธีแก้ไขปัญหาขีด จำกัด หน่วยความจำคือการคอมไพล์แบบ 64 บิต

อย่างไรก็ตามขนาดของวัตถุใน Visual Studio ยังคงถูก จำกัด ไว้ที่ 2GB โดยค่าเริ่มต้น คุณจะสามารถสร้างอาร์เรย์หลายอาร์เรย์ที่มีขนาดรวมกันมากกว่า 2GB แต่โดยค่าเริ่มต้นคุณไม่สามารถสร้างอาร์เรย์ที่ใหญ่กว่า 2GB ได้ หวังว่าหากคุณยังต้องการสร้างอาร์เรย์ที่ใหญ่กว่า 2GB คุณสามารถทำได้โดยเพิ่มรหัสต่อไปนี้ในไฟล์ app.config ของคุณ:

<configuration>
  <runtime>
    <gcAllowVeryLargeObjects enabled="true" />
  </runtime>
</configuration>

คุณช่วยฉันไว้. ขอบคุณ!
Tee Zad Awk

25

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

  1. คุณสามารถลองใช้งาน / 3GB (ตามที่คนอื่นแนะนำ)
  2. หรือเปลี่ยนไปใช้ระบบปฏิบัติการ 64 บิต
  3. หรือปรับเปลี่ยนอัลกอริทึมเพื่อให้ไม่ต้องใช้หน่วยความจำขนาดใหญ่ อาจจัดสรรหน่วยความจำที่เล็กกว่า (ค่อนข้าง) เล็กน้อย

7

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

int sizeA = 10000,
    sizeB = 10000;
double sizeInMegabytes = (sizeA * sizeB * 8.0) / 1024.0 / 1024.0; //762 mb
double[][] randomNumbers = new double[sizeA][];
for (int i = 0; i < randomNumbers.Length; i++)
{
    randomNumbers[i] = new double[sizeB];
}

randomNumbers[i / sizeB][i % sizeB]จากนั้นจะได้รับดัชนีโดยเฉพาะอย่างยิ่งที่คุณจะใช้

อีกทางเลือกหนึ่งหากคุณเข้าถึงค่าตามลำดับตลอดเวลาอาจใช้ตัวสร้างที่โอเวอร์โหลดเพื่อระบุเมล็ดพันธุ์ ด้วยวิธีนี้คุณจะได้รับตัวเลขกึ่งสุ่ม (เช่นDateTime.Now.Ticks) เก็บไว้ในตัวแปรจากนั้นเมื่อคุณเริ่มดูรายการคุณจะสร้างอินสแตนซ์สุ่มใหม่โดยใช้เมล็ดพันธุ์ดั้งเดิม:

private static int randSeed = (int)DateTime.Now.Ticks;  //Must stay the same unless you want to get different random numbers.
private static Random GetNewRandomIterator()
{
    return new Random(randSeed);
}

สิ่งสำคัญคือต้องทราบว่าในขณะที่บล็อกที่เชื่อมโยงในคำตอบของ Fredrik Mörkระบุว่าปัญหามักเกิดจากการไม่มีพื้นที่ที่อยู่แต่ก็ไม่ได้แสดงรายการปัญหาอื่น ๆ เช่นการ จำกัด ขนาดวัตถุ 2GB CLR (กล่าวถึงในความคิดเห็นจาก ShuggyCoUk ในบล็อกเดียวกัน) ปัดสวะเรื่องการกระจายตัวของหน่วยความจำและไม่ได้กล่าวถึงผลกระทบของขนาดไฟล์เพจ (และวิธีแก้ไขด้วยการใช้CreateFileMappingฟังก์ชัน )

ข้อ จำกัด 2GB หมายความว่าrandomNumbers ต้องน้อยกว่า 2GB เนื่องจากอาร์เรย์เป็นคลาสและมีค่าใช้จ่ายอยู่บ้างจึงทำให้อาร์เรย์ของdoubleจะต้องมีขนาดเล็กกว่า 2 ^ 31 ฉันไม่แน่ใจว่า 2 ^ 31 ความยาวจะต้องเล็กลงแค่ไหน แต่ค่าใช้จ่ายของอาร์เรย์. NET? บ่งชี้ 12 - 16 ไบต์

การกระจายตัวของหน่วยความจำคล้ายกับการกระจายตัวของ HDD มาก คุณอาจมีพื้นที่ที่อยู่ 2GB แต่เมื่อคุณสร้างและทำลายวัตถุจะมีช่องว่างระหว่างค่าต่างๆ หากช่องว่างเหล่านี้เล็กเกินไปสำหรับวัตถุขนาดใหญ่ของคุณและไม่สามารถขอพื้นที่เพิ่มเติมได้คุณจะได้รับไฟล์System.OutOfMemoryException. ตัวอย่างเช่นหากคุณสร้างวัตถุ 2 ล้าน 1024 ไบต์แสดงว่าคุณกำลังใช้ 1.9GB หากคุณลบทุกออบเจ็กต์โดยที่แอดเดรสไม่ใช่ผลคูณของ 3 คุณจะใช้หน่วยความจำ. 6GB แต่จะกระจายออกไปตามพื้นที่แอดเดรสโดยมีบล็อคเปิด 2024 ไบต์อยู่ระหว่าง หากคุณต้องการสร้างวัตถุที่มีขนาด. 2GB คุณจะไม่สามารถทำได้เนื่องจากไม่มีบล็อกที่ใหญ่พอที่จะใส่เข้าไปได้และไม่สามารถหาพื้นที่เพิ่มเติมได้ (สมมติว่าเป็นสภาพแวดล้อม 32 บิต) วิธีแก้ไขที่เป็นไปได้สำหรับปัญหานี้ ได้แก่ การใช้วัตถุขนาดเล็กลดจำนวนข้อมูลที่คุณจัดเก็บในหน่วยความจำหรือใช้อัลกอริธึมการจัดการหน่วยความจำเพื่อ จำกัด / ป้องกันการแตกตัวของหน่วยความจำ ควรสังเกตว่าหากคุณไม่ได้พัฒนาโปรแกรมขนาดใหญ่ซึ่งใช้หน่วยความจำจำนวนมากสิ่งนี้จะไม่เป็นปัญหา นอกจากนี้

เนื่องจากโปรแกรมส่วนใหญ่ร้องขอหน่วยความจำที่ใช้งานได้จากระบบปฏิบัติการและไม่ขอการแมปไฟล์โปรแกรมเหล่านี้จะถูก จำกัด โดย RAM ของระบบและขนาดไฟล์เพจ ตามที่ระบุไว้ในความคิดเห็นของNéstorSánchez (NéstorSánchez) ในบล็อกด้วยรหัสที่มีการจัดการเช่น C # คุณจะติดอยู่กับข้อ จำกัด ของไฟล์ RAM / page และพื้นที่ที่อยู่ของระบบปฏิบัติการ


นั่นเป็นวิธีที่คาดไว้นานกว่านั้น หวังว่าจะช่วยใครบางคนได้ ฉันโพสต์เพราะฉันพบกับการSystem.OutOfMemoryExceptionรันโปรแกรม x64 บนระบบที่มี RAM 24GB แม้ว่าอาร์เรย์ของฉันจะเก็บของได้เพียง 2GB ก็ตาม


5

ฉันไม่แนะนำให้ใช้ตัวเลือกการบูต Windows / 3GB นอกเหนือจากทุกอย่างอื่น (มัน overkill ทำเช่นนี้สำหรับหนึ่งในการประยุกต์ใช้ความประพฤติไม่ดีและมันอาจจะไม่แก้ปัญหาของคุณอยู่แล้ว) มันมากอาจทำให้เกิดความไม่แน่นอน

ไดรเวอร์ Windows จำนวนมากไม่ได้รับการทดสอบด้วยตัวเลือกนี้ดังนั้นจึงมีไม่กี่ตัวที่คิดว่าตัวชี้โหมดผู้ใช้จะชี้ไปที่พื้นที่ที่อยู่ที่ต่ำกว่า 2GB เสมอ ซึ่งหมายความว่าอาจแตกอย่างน่ากลัวด้วย / 3GB

อย่างไรก็ตามโดยปกติ Windows จะ จำกัด กระบวนการ 32 บิตไว้ที่พื้นที่แอดเดรส 2GB แต่ไม่ได้หมายความว่าคุณควรคาดหวังว่าจะสามารถจัดสรร 2GB ได้!

พื้นที่ที่อยู่เต็มไปด้วยข้อมูลที่จัดสรรทุกประเภทแล้ว มีสแต็กและชุดประกอบทั้งหมดที่โหลดตัวแปรคงที่และอื่น ๆ ไม่มีการรับประกันว่าจะมีหน่วยความจำที่ไม่ได้ปันส่วนต่อเนื่องกัน 800MB จากที่ใดก็ได้

การจัดสรรชิ้นส่วน 400MB 2 ชิ้นน่าจะดีกว่า หรือ 4 ชิ้น 200MB การจัดสรรที่น้อยกว่านั้นง่ายกว่ามากในการหาที่ว่างในพื้นที่หน่วยความจำที่กระจัดกระจาย

อย่างไรก็ตามหากคุณจะปรับใช้สิ่งนี้กับเครื่อง 12GB คุณจะต้องเรียกใช้สิ่งนี้เป็นแอปพลิเคชัน 64 บิตซึ่งจะช่วยแก้ปัญหาทั้งหมดได้


การแบ่งงานออกเป็นชิ้นเล็ก ๆ ดูเหมือนจะไม่ช่วยให้เห็นการอัปเดตของฉันด้านบน
m3ntat

4

การเปลี่ยนจาก 32 เป็น 64 บิตเหมาะสำหรับฉัน - ควรลองถ้าคุณใช้พีซี 64 บิตและไม่จำเป็นต้องพอร์ต



1

หน้าต่าง 32 บิตมีขีด จำกัด หน่วยความจำในกระบวนการ 2GB ตัวเลือกการบูต / 3GB อื่น ๆ ได้กล่าวถึงจะทำให้ 3GB นี้เหลือเพียง 1gb สำหรับการใช้งานเคอร์เนลระบบปฏิบัติการ ตามความเป็นจริงหากคุณต้องการใช้งานมากกว่า 2GB โดยไม่ยุ่งยากจำเป็นต้องใช้ระบบปฏิบัติการ 64 บิต นอกจากนี้ยังช่วยแก้ปัญหาด้วยแม้ว่าคุณอาจมี RAM จริง 4GB แต่พื้นที่แอดเดรสที่ต้องการสำหรับการ์ดแสดงผลอาจทำให้หน่วยความจำขนาดใหญ่ไม่สามารถใช้งานได้โดยปกติจะอยู่ที่ประมาณ 500MB


1

แทนที่จะจัดสรรอาร์เรย์ขนาดใหญ่คุณลองใช้ตัววนซ้ำได้ไหม สิ่งเหล่านี้มีการดำเนินการล่าช้าหมายความว่าค่าจะถูกสร้างขึ้นตามที่ร้องขอในคำสั่ง foreach เท่านั้น คุณไม่ควรใช้หน่วยความจำจนหมดด้วยวิธีนี้:

private static IEnumerable<double> MakeRandomNumbers(int numberOfRandomNumbers) 
{
    for (int i = 0; i < numberOfRandomNumbers; i++)
    {
        yield return randomGenerator.GetAnotherRandomNumber();
    }
}


...

// Hooray, we won't run out of memory!
foreach(var number in MakeRandomNumbers(int.MaxValue))
{
    Console.WriteLine(number);
}

ข้างต้นจะสร้างตัวเลขสุ่มได้มากเท่าที่คุณต้องการ แต่จะสร้างเฉพาะตามที่ขอผ่านคำสั่ง foreach คุณจะไม่เหลือหน่วยความจำแบบนั้น

อีกวิธีหนึ่งหากคุณต้องมีทั้งหมดในที่เดียวให้เก็บไว้ในไฟล์แทนที่จะอยู่ในหน่วยความจำ


แนวทางที่น่าสนใจ แต่ฉันต้องจัดเก็บให้ได้มากที่สุดเท่าที่จะเป็นไปได้ในที่เก็บตัวเลขแบบสุ่มในช่วงเวลาที่ไม่ได้ใช้งานของแอปพลิเคชันที่เหลือของฉันเนื่องจากแอปนี้ทำงานบนนาฬิกา 24 ชั่วโมงซึ่งรองรับพื้นที่ทางภูมิศาสตร์หลายแห่ง (การจำลองมอนติคาร์โลหลายแบบ) ประมาณ 70% ของการโหลดซีพียูสูงสุดของวันเวลาที่เหลือตลอดทั้งวันฉันต้องการบัฟเฟอร์ตัวเลขสุ่มในพื้นที่หน่วยความจำว่างทั้งหมด การจัดเก็บลงดิสก์ช้าเกินไปและเป็นการเอาชนะผลกำไรใด ๆ ที่ฉันสามารถทำบัฟเฟอร์ในแคชหน่วยความจำตัวเลขสุ่มนี้ได้
m3ntat

0

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


ที่จริงฉันรู้การกำหนดค่าของเครื่องนี้ทำงานบนเซิร์ฟเวอร์เดียวเท่านั้นและฉันสามารถเขียนสิ่งนี้สำหรับข้อกำหนดเหล่านั้นได้ นี่คือการจำลองมอนติคาร์โลขนาดใหญ่และฉันกำลังพยายามเพิ่มประสิทธิภาพโดยการบัฟเฟอร์ตัวเลขสุ่มล่วงหน้า
m3ntat

0

ฉันมีปัญหาที่คล้ายกันมันเกิดจาก StringBuilder ToString ();


อย่าปล่อยให้นักสตริงใหญ่ขนาดนี้
Ricardo Rix

0

แปลงโซลูชันของคุณเป็น x64 หากคุณยังคงประสบปัญหาให้กำหนดความยาวสูงสุดสำหรับทุกสิ่งที่มีข้อยกเว้นดังต่อไปนี้:

 var jsSerializer = new JavaScriptSerializer();
 jsSerializer.MaxJsonLength = Int32.MaxValue;

0

หากคุณไม่ต้องการกระบวนการโฮสติ้ง Visual Studio:

ยกเลิกการเลือกตัวเลือก: Project-> Properties-> Debug-> Enable the Visual Studio Hosting Process

แล้วสร้าง

หากคุณยังคงประสบปัญหา:

ไปที่ Project-> Properties-> Build Events-> Post-Build Event Command line และวางข้อมูลต่อไปนี้:

call "$(DevEnvDir)..\..\vc\vcvarsall.bat" x86
"$(DevEnvDir)..\..\vc\bin\EditBin.exe" "$(TargetPath)"  /LARGEADDRESSAWARE

ตอนนี้สร้างโครงการ


-2

เพิ่มขีด จำกัด กระบวนการ Windows เป็น 3gb (ผ่าน boot.ini หรือ Vista boot manager)


จริงๆ? หน่วยความจำกระบวนการสูงสุดเริ่มต้นคืออะไร? และจะเปลี่ยนได้อย่างไร? หากฉันเล่นเกมหรืออะไรบางอย่างบนพีซีของฉันมันสามารถใช้ 2+ GB จาก EXE / กระบวนการเดียวได้อย่างง่ายดายฉันไม่คิดว่านี่จะเป็นปัญหาที่นี่
m3ntat

/ 3GB นั้นมากเกินไปสำหรับสิ่งนี้และอาจทำให้เกิดความไม่เสถียรได้มากเนื่องจากไดรเวอร์หลาย ๆ ตัวคิดว่าตัวชี้พื้นที่ผู้ใช้จะชี้ไปที่ 2GB ที่ต่ำกว่าเสมอ
jalf

1
m3ntat: ไม่ใน Windows 32 บิตกระบวนการเดียวถูก จำกัด ไว้ที่ 2GB เคอร์เนลใช้พื้นที่ที่อยู่ 2GB ที่เหลืออยู่
jalf

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.