שימוש בשרשורים של WebAssembly מ-C, C++ ו-Rust

איך מעבירים אפליקציות עם כמה שרשורים שנכתבו בשפות אחרות ל-WebAssembly?

תמיכה בשרשור של WebAssembly היא אחת מהתוספות החשובות ביותר לשיפור הביצועים של WebAssembly. היא מאפשרת להריץ חלקים מהקוד במקביל בליבות נפרדות, או את אותו קוד בחלקים עצמאיים של נתוני הקלט, ולשנות את ההתאמה שלו לכמות הליבות שיש למשתמש, וכך לצמצם באופן משמעותי את משך הביצוע הכולל.

במאמר הזה נסביר איך להשתמש בשרשראות של WebAssembly כדי להעביר לאינטרנט אפליקציות עם כמה שרשורים שנכתבו בשפות כמו C,‏ C++‎ ו-Rust.

שרשראות של WebAssembly הן לא תכונה נפרדת, אלא שילוב של כמה רכיבים שמאפשר לאפליקציות WebAssembly להשתמש במודלים מסורתיים של ריבוי שרשורים באינטרנט.

Web Workers

הרכיב הראשון הוא Workers הרגילים שאתם מכירים ומעריכים מ-JavaScript. בשרתי WebAssembly, ה-constructor new Worker משמש ליצירת שרשורים בסיסיים חדשים. כל שרשור טוען דבק JavaScript, ואז השרשור הראשי משתמש בשיטה Worker#postMessage כדי לשתף את WebAssembly.Module המקודד וגם את WebAssembly.Memory המשותף (ראו בהמשך) עם שאר השרשורים. כך נוצרת תקשורת ומאפשרת לכל השרשור האלה להריץ את אותו קוד WebAssembly באותה זיכרון משותף, בלי לעבור שוב דרך JavaScript.

Web Workers קיימים כבר יותר מעשור, יש להם תמיכה רחבה והם לא דורשים דגלים מיוחדים.

SharedArrayBuffer

הזיכרון של WebAssembly מיוצג על ידי אובייקט WebAssembly.Memory בממשק ה-API של JavaScript. כברירת מחדל, WebAssembly.Memory הוא מעטפת של ArrayBuffer – מאגר נתונים זמני של בייטים גולמיים שרק לשרשור אחד יש גישה אליו.

> new WebAssembly.Memory({ initial:1, maximum:10 }).buffer
ArrayBuffer {  }

כדי לתמוך במספר תהליכים בו-זמנית, נוספה ל-WebAssembly.Memory גם וריאנט משותף. כשהיא נוצרת עם הדגל shared דרך JavaScript API, או על ידי קובץ ה-WebAssembly הבינארי עצמו, היא הופכת לעטיפה של SharedArrayBuffer. זוהי וריאציה של ArrayBuffer שאפשר לשתף עם שרשורים אחרים ולקרוא או לשנות אותה בו-זמנית משני הצדדים.

> new WebAssembly.Memory({ initial:1, maximum:10, shared:true }).buffer
SharedArrayBuffer {  }

בניגוד ל-postMessage, שמשמש בדרך כלל לתקשורת בין הליבה לבין משימות ה-Web Worker, ב-SharedArrayBuffer אין צורך להעתיק נתונים או אפילו להמתין ל-event loop כדי לשלוח ולקבל הודעות. במקום זאת, כל השינויים גלויים לכל השרשור כמעט באופן מיידי, כך שזוהי יעד הידור הרבה יותר טוב לפרימיטיבים מסורתיים של סנכרון.

ל-SharedArrayBuffer יש היסטוריה מורכבת. הוא שוחרר לראשונה בכמה דפדפנים באמצע 2017, אבל נאלצנו להשבית אותו בתחילת 2018 בגלל גילוי נקודות חולשה של Spectre. הסיבה הספציפית לכך היא שחילוץ הנתונים ב-Spectre מתבסס על התקפות תזמון – מדידת זמן הביצוע של קטע קוד מסוים. כדי להקשות על התקפה כזו, הדפדפנים הפחיתו את הדיוק של ממשקי ה-API הרגילים לניהול תזמון, כמו Date.now ו-performance.now. עם זאת, זיכרון משותף בשילוב עם לולאת ספירה פשוטה שפועלת בשרשור נפרד היא גם דרך אמינה מאוד לקבל תזמון מדויק, וקשה הרבה יותר לצמצם את השימוש בה בלי לצמצם באופן משמעותי את הביצועים בסביבת זמן הריצה.

במקום זאת, בגרסה 68 של Chrome (אמצע 2018) הפעלנו מחדש את SharedArrayBuffer באמצעות Site Isolation – תכונה שמציבה אתרים שונים בתהליכים שונים ומקשה מאוד להשתמש בהתקפות בערוץ צדדי כמו Spectre. עם זאת, הפעולה הזו הייתה מוגבלת רק ל-Chrome למחשב, כי בידוד האתרים הוא תכונה יקרה למדי, ולא ניתן להפעיל אותה כברירת מחדל לכל האתרים במכשירים ניידים עם זיכרון נמוך, וגם עדיין לא ספקים אחרים הטמיעו אותה.

הגענו לשנת 2020, ו-Chrome ו-Firefox כוללים הטמעות של בידוד אתרים, ויש דרך רגילה לאתרים להביע הסכמה לשימוש בתכונה באמצעות כותרות COOP ו-COEP. מנגנון הסכמה מאפשר להשתמש בבידוד של אתר גם במכשירים עם הספק נמוך, שבהם הפעלת הבידוד לכל האתרים יקרה מדי. כדי להביע הסכמה, מוסיפים את הכותרות הבאות למסמך הראשי בהגדרות השרת:

Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin

אחרי שתאשרו את ההסכמה, תקבלו גישה ל-SharedArrayBuffer (כולל WebAssembly.Memory שמגודר על ידי SharedArrayBuffer), לטיימרים מדויקים, למדידת זיכרון ולממשקי API אחרים שדורשים מקור מבודד מטעמי אבטחה. לפרטים נוספים, קראו את המאמר בידוד האתר מ-origin שונים באמצעות COOP ו-COEP.

פעולות אטומיות ב-WebAssembly

אמנם SharedArrayBuffer מאפשר לכל שרשור לקרוא ולכתוב באותה זיכרון, אבל כדי שהתקשורת תהיה תקינה, צריך לוודא שהם לא מבצעים פעולות סותרות בו-זמנית. לדוגמה, יכול להיות ששרשור אחד יתחיל לקרוא נתונים מכתובת משותפת בזמן ששרשור אחר כותב אליה, כך שהשרשור הראשון יקבל עכשיו תוצאה פגומה. קטגוריית הבאגים הזו נקראת תנאי מרוץ. כדי למנוע מרוץ תהליכים, צריך לסנכרן את הגישה הזו. כאן נכנסות לתמונה פעולות אטומיות.

WebAssembly atomics הוא תוסף לחבילת ההוראות של WebAssembly שמאפשר לקרוא ולכתוב תאים קטנים של נתונים (בדרך כלל מספרים שלמים של 32 ו-64 ביט) באופן 'אטומי'. כלומר, באופן שמבטיח שאף שני חוטים לא קוראים או כותבים באותו תא בו-זמנית, וכך מונעים התנגשויות כאלה ברמה נמוכה. בנוסף, פונקציות האטומיות של WebAssembly מכילות עוד שני סוגים של הוראות – 'wait' ו-'notify' – שמאפשרות לשרשור אחד להיכנס למצב שינה ("wait") בכתובת מסוימת בזיכרון משותף, עד ששרשור אחר יעיר אותו באמצעות 'notify'.

כל האלמנטים הפשוטים ברמה גבוהה יותר של תזמון, כולל ערוצים, מנעולים מרובים לצורך נעילה-פתיחה (mutex) ומנעולים לקריאה וכתיבה, מבוססים על ההוראות האלה.

איך משתמשים בשרשור של WebAssembly

זיהוי תכונות

SharedArrayBuffer ו-WebAssembly atomics הן תכונות חדשות יחסית, שעדיין לא זמינות בכל הדפדפנים עם תמיכה ב-WebAssembly. בתוכנית העבודה של webassembly.org תוכלו למצוא אילו דפדפנים תומכים בתכונות החדשות של WebAssembly.

כדי לוודא שכל המשתמשים יוכלו לטעון את האפליקציה, תצטרכו להטמיע שיפורים הדרגתיים על ידי פיתוח שתי גרסאות שונות של Wasm – אחת עם תמיכה במספר תהליכים בו-זמנית ואחת בלי. לאחר מכן, תוכלו לטעון את הגרסה הנתמכת בהתאם לתוצאות זיהוי התכונות. כדי לזהות תמיכה בשרתי WebAssembly בסביבת זמן הריצה, משתמשים ב-wasm-feature-detect library ומטעינים את המודול כך:

import { threads } from 'wasm-feature-detect';

const hasThreads = await threads();

const module = await (
  hasThreads
    ? import('./module-with-threads.js')
    : import('./module-without-threads.js')
);

// …now use `module` as you normally would

עכשיו נראה איך יוצרים גרסה עם כמה שרשורים של מודול WebAssembly.

C

ב-C, במיוחד במערכות שדומות ל-Unix, הדרך הנפוצה להשתמש בשרשור היא באמצעות POSIX שרשור שמסופק על ידי הספרייה pthread. Emscripten מספק הטמעה תואמת-API של הספרייה pthread שנבנתה על גבי Web Workers, זיכרון משותף ואטומיים, כך שאותו קוד יכול לפעול באינטרנט ללא שינויים.

בואו נראה דוגמה:

example.c:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void *thread_callback(void *arg)
{
    sleep(1);
    printf("Inside the thread: %d\n", *(int *)arg);
    return NULL;
}

int main()
{
    puts("Before the thread");

    pthread_t thread_id;
    int arg = 42;
    pthread_create(&thread_id, NULL, thread_callback, &arg);

    pthread_join(thread_id, NULL);

    puts("After the thread");

    return 0;
}

כאן הכותרות של ספריית pthread כלולות דרך pthread.h. תוכלו גם לראות כמה פונקציות חיוניות לטיפול בשרשורים.

pthread_create תיצור פעילות ברקע. הפונקציה מקבלת יעד לאחסון ה-handle של השרשור, מאפיינים מסוימים ליצירת שרשור (כאן לא מעבירים אף מאפיין, כך שהערך הוא פשוט NULL), את הפונקציה הלא חוזרת (callback) שתתבצע בשרשור החדש (כאן thread_callback) ואת הפונקציה הלא חוזרת (callback) האופציונלית להעברת הארגומנטים, למקרה שרוצים לשתף נתונים מהשרשור הראשי. בדוגמה הזו אנחנו משתפים את הפונקציה הלא חוזרת (callback) של המשתנה arg.

אפשר להפעיל את pthread_join בכל שלב מאוחר יותר כדי להמתין לסיום ההרצה של ה-thread ולקבל את התוצאה שהוחזרה מה-callback. הוא מקבל את ה-handle של השרשור שהוקצה קודם, וכן מצביע לאחסון התוצאה. במקרה הזה, אין תוצאות, ולכן הפונקציה מקבלת את הערך NULL כארגומנט.

כדי לקמפל קוד באמצעות חוטים ב-Emscripten, צריך להפעיל את emcc ולהעביר פרמטר -pthread, כמו כשמקמפלים את אותו קוד באמצעות Clang או GCC בפלטפורמות אחרות:

emcc -pthread example.c -o example.js

עם זאת, כשמנסים להריץ אותו בדפדפן או ב-Node.js, מופיעה אזהרה ואז התוכנית נתקעת:

Before the thread
Tried to spawn a new thread, but the thread pool is exhausted.
This might result in a deadlock unless some threads eventually exit or the code
explicitly breaks out to the event loop.
If you want to increase the pool size, use setting `-s PTHREAD_POOL_SIZE=...`.
If you want to throw an explicit error instead of the risk of deadlocking in those
cases, use setting `-s PTHREAD_POOL_SIZE_STRICT=2`.
[…hangs here…]

מה קרה? הבעיה היא שרוב ממשקי ה-API באינטרנט שדורשים זמן רב הם אסינכרונים ומסתמכים על לולאת האירועים כדי לפעול. ההגבלה הזו היא הבדל חשוב בהשוואה לסביבות מסורתיות, שבהן אפליקציות מריצות בדרך כלל קלט/פלט באופן סינכרוני וחסום. למידע נוסף, מומלץ לקרוא את הפוסט בבלוג בנושא שימוש בממשקי API אסינכררוניים של אינטרנט מ-WebAssembly.

במקרה כזה, הקוד קורא לסינכרון ל-pthread_create כדי ליצור שרשור ברקע, ולאחר מכן מבצע קריאה סינכרנית נוספת ל-pthread_join שמחכה לסיום ההרצה של שרשור הרקע. עם זאת, משימות Web Workers, שמשמשות מאחורי הקלעים כשהקוד הזה עובר הידור באמצעות Emscripten, הן אסינכרוניות. מה שקורה הוא ש-pthread_create רק מתזמן את היצירה של חוט עבודה חדש בזמן ההפעלה הבא של לולאת האירועים, אבל לאחר מכן pthread_join חוסם מיד את לולאת האירועים כדי להמתין לאותו Worker, וכך מונע את היצירה שלו. זוהי דוגמה קלאסית לנעילה גורפת.

אחת הדרכים לפתור את הבעיה הזו היא ליצור מאגר של Workers מראש, עוד לפני שהתוכנית מתחילה לפעול. כשמפעילים את pthread_create, הוא יכול לקחת מהמאגר Worker מוכן לשימוש, להריץ את פונקציית ה-callback שסופקה בשרשור הרקע שלו ולהחזיר את ה-Worker למאגר. אפשר לבצע את כל הפעולות האלה באופן סינכרוני, כך שלא יהיו נעילות מרובות (deadlocks) כל עוד המאגר גדול מספיק.

זה בדיוק מה ש-Emscripten מאפשר באמצעות האפשרות -s PTHREAD_POOL_SIZE=.... היא מאפשרת לציין מספר של שרשורים – מספר קבוע או ביטוי JavaScript כמו navigator.hardwareConcurrency כדי ליצור כמה שרשורים שיש ליבות ב-CPU. האפשרות השנייה שימושית כשהקוד יכול להתאים למספר שרירותי של חוטים.

בדוגמה שלמעלה, נוצר רק חוט אחד, ולכן במקום להקצות את כל הליבות, מספיק להשתמש ב--s PTHREAD_POOL_SIZE=1:

emcc -pthread -s PTHREAD_POOL_SIZE=1 example.c -o example.js

הפעם, כשמריצים את הפקודה, הכל עובד כמו שצריך:

Before the thread
Inside the thread: 42
After the thread
Pthread 0x701510 exited.

אבל יש בעיה נוספת: רואים את הערך sleep(1) בדוגמה לקוד? הוא מופעל ב-thread callback, כלומר מחוץ ל-thread הראשי, אז זה אמור להיות בסדר, נכון? לא, זה לא.

כשקוראים ל-pthread_join, היא צריכה להמתין לסיום ביצוע ה-thread. כלומר, אם ה-thread שנוצר מבצע משימות ממושכות – במקרה הזה, השהיה של שנייה אחת – ה-thread הראשי יצטרך לחסום גם הוא למשך אותו פרק זמן עד שהתוצאות יחזרו. כשקוד ה-JS הזה יבוצע בדפדפן, הוא יחסום את שרשור ממשק המשתמש למשך שנייה אחת עד שהקריאה החוזרת (callback) של השרשור תוחזר. התוצאה היא חוויית משתמש גרועה.

יש כמה פתרונות לבעיה הזו:

  • pthread_detach
  • -s PROXY_TO_PTHREAD
  • 'עובד' ו'קשר' בהתאמה אישית

pthread_detach

ראשית, אם אתם צריכים להריץ רק משימות מסוימות מחוץ לשרשור הראשי, אבל לא צריכים להמתין לתוצאות, תוכלו להשתמש ב-pthread_detach במקום ב-pthread_join. כך פונקציית ה-callback של השרשור תמשיך לפעול ברקע. אם אתם משתמשים באפשרות הזו, תוכלו להשבית את האזהרה באמצעות -s PTHREAD_POOL_SIZE_STRICT=0.

PROXY_TO_PTHREAD

שנית, אם אתם עורכים הידור של אפליקציית C ולא של ספרייה, תוכלו להשתמש באפשרות -s PROXY_TO_PTHREAD, שתעביר את קוד האפליקציה הראשי לשרשור נפרד, בנוסף לכל השרשורים המטמיעים שנוצרו על ידי האפליקציה עצמה. כך הקוד הראשי יכול לחסום בבטחה בכל שלב בלי להקפיא את ממשק המשתמש. דרך אגב, כשמשתמשים באפשרות הזו, גם לא צריך ליצור מראש את מאגר השרשור. במקום זאת, Emscripten יכול להשתמש בשרשור הראשי כדי ליצור עובדים חדשים ברקע, ואז לחסום את שרשור העזר ב-pthread_join בלי לגרום לנעילה מרובת משתתפים.

שלישית, אם אתם עובדים על ספרייה ועדיין צריכים לבצע חסימה, תוכלו ליצור Worker משלכם, לייבא את הקוד שנוצר על ידי Emscripten ולהציג אותו לשרשור הראשי באמצעות Comlink. כך, ה-thread הראשי יוכל להפעיל כל שיטות הייצוא כפונקציות אסינכרוניות, וכך גם למנוע חסימה של ממשק המשתמש.

באפליקציה פשוטה כמו הדוגמה הקודמת, -s PROXY_TO_PTHREAD היא האפשרות הטובה ביותר:

emcc -pthread -s PROXY_TO_PTHREAD example.c -o example.js

C++‎

כל אותם אזהרות וההיגיון רלוונטיים ל-C++‎ באותה צורה. הדבר היחיד החדש שאתם מקבלים הוא גישה לממשקי API ברמה גבוהה יותר, כמו std::thread ו-std::async, שמשתמשים בספרייה pthread שצוינה למעלה.

לכן אפשר לכתוב מחדש את הדוגמה שלמעלה בקוד C++‎ שמתאים יותר לסגנון הכתיבה:

example.cpp:

#include <iostream>
#include <thread>
#include <chrono>

int main()
{
    puts("Before the thread");

    int arg = 42;
    std::thread thread([&]() {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::cout << "Inside the thread: " << arg << std::endl;
    });

    thread.join();

    std::cout << "After the thread" << std::endl;

    return 0;
}

כשהקוד יעבור הידור ויופעל עם פרמטרים דומים, הוא יפעל באותו אופן כמו הדוגמה ב-C:

emcc -std=c++11 -pthread -s PROXY_TO_PTHREAD example.cpp -o example.js

פלט:

Before the thread
Inside the thread: 42
Pthread 0xc06190 exited.
After the thread
Proxied main thread 0xa05c18 finished with return code 0. EXIT_RUNTIME=0 set, so
keeping main thread alive for asynchronous event operations.
Pthread 0xa05c18 exited.

Rust

בניגוד ל-Emscripten, ל-Rust אין יעד אינטרנט מותאם אישית מקצה לקצה, אלא יעד wasm32-unknown-unknown כללי לפלט WebAssembly כללי.

אם אתם מתכננים להשתמש ב-Wasm בסביבת אינטרנט, כל אינטראקציה עם ממשקי API של JavaScript תתבצע באמצעות ספריות וכלים חיצוניים כמו wasm-bindgen ו-wasm-pack. לצערנו, המשמעות היא שהספרייה הרגילה לא מכירה ב-Web Workers, ו-API רגילים כמו std::thread לא יפעלו כשהם יקובצו ל-WebAssembly.

למרבה המזל, רוב הסביבה העסקית מסתמכת על ספריות ברמה גבוהה יותר שמטפלות בשרשור משימות. ברמה הזו קל יותר להתמקד בעיקר ולהתעלם מהבדלים בין הפלטפורמות.

במיוחד, Rayon הוא האפשרות הפופולרית ביותר לעבודה במקביל על נתונים ב-Rust. היא מאפשרת להשתמש ברשתות שיטות במערכי איטרטור רגילים, ובדרך כלל באמצעות שינוי שורה אחת, להמיר אותן כך שיפעלו במקביל בכל השרשוריים הזמינים במקום ברצף. לדוגמה:

pub fn sum_of_squares(numbers: &[i32]) -> i32 {
  numbers
  .iter()
  .par_iter()
  .map(|x| x * x)
  .sum()
}

בעקבות השינוי הקטן הזה, הקוד יחלק את נתוני הקלט, יחשב את x * x ואת הסכומים החלקים בשרשוריים מקבילים, ובסוף יסכם את התוצאות החלקיות האלה.

כדי להתאים לפלטפורמות שבהן std::thread לא פועל, ב-Rayon יש ווקים שמאפשרים להגדיר לוגיקה מותאמת אישית ליצירה וליציאה של חוטים.

wasm-bindgen-rayon משתמש בווקריטים האלה כדי ליצור חוטי WebAssembly כ-Web Workers. כדי להשתמש בו, צריך להוסיף אותו כיחס תלות ולפעול לפי שלבי ההגדרה שמפורטים במסמכי העזרה. הדוגמה שלמעלה תיראה כך:

pub use wasm_bindgen_rayon::init_thread_pool;

#[wasm_bindgen]
pub fn sum_of_squares(numbers: &[i32]) -> i32 {
  numbers
  .par_iter()
  .map(|x| x * x)
  .sum()
}

בסיום, קוד ה-JavaScript שנוצר ייצא פונקציית initThreadPool נוספת. הפונקציה הזו תיצור מאגר של Workers ותשתמש בהם שוב ושוב במהלך כל משך החיים של התוכנית, לכל פעולה עם כמה שרשורים שמתבצעת על ידי Rayon.

מנגנון המאגר הזה דומה לאפשרות -s PTHREAD_POOL_SIZE=... ב-Emscripten, כפי שמוסבר למעלה. גם הוא צריך לעבור את תהליך האיפוס לפני הקוד הראשי כדי למנוע נעילה גורפת:

import init, { initThreadPool, sum_of_squares } from './pkg/index.js';

// Regular wasm-bindgen initialization.
await init();

// Thread pool initialization with the given number of threads
// (pass `navigator.hardwareConcurrency` if you want to use all cores).
await initThreadPool(navigator.hardwareConcurrency);

// ...now you can invoke any exported functions as you normally would
console.log(sum_of_squares(new Int32Array([1, 2, 3]))); // 14

חשוב לזכור שגם כאן חלים אותם אזהרות לגבי חסימה של ה-thread הראשי. גם בדוגמה sum_of_squares עדיין צריך לחסום את ה-thread הראשי כדי להמתין לתוצאות החלקיות מה-threads האחרים.

זמן ההמתנה עשוי להיות קצר או ארוך, בהתאם למורכבות של האינטרטוררים ומספר השרשוריים הזמינים, אבל כדי להימנע מבעיות, מנועי הדפדפנים מונעים באופן פעיל חסימה של השרשור הראשי לגמרי, וקובץ קוד כזה יגרום להצגת שגיאה. במקום זאת, צריך ליצור Worker, לייבא אליו את הקוד שנוצר על ידי wasm-bindgen ולהציג את ה-API שלו לשרשור הראשי באמצעות ספרייה כמו Comlink.

כדאי לעיין בדוגמה של wasm-bindgen-rayon כדי לראות הדגמה מקצה לקצה שמראה:

תרחישים לדוגמה מהעולם האמיתי

אנחנו משתמשים באופן פעיל בשרתי WebAssembly ב-Squoosh.app כדי לדחוס תמונות בצד הלקוח, במיוחד בפורמטים כמו AVIF‏ (C++), ‏ JPEG-XL‏ (C++), ‏ OxiPNG‏ (Rust) ו-WebP v2‏ (C++). בזכות השימוש בכמה שרתים בו-זמנית בלבד, הצלחנו לשפר את המהירות באופן עקבי פי 1.5 עד פי 3 (היחס המדויק משתנה בהתאם לקודק), והצלחנו לשפר את המספרים האלה עוד יותר על ידי שילוב של שרתים של WebAssembly עם WebAssembly SIMD.

Google Earth הוא שירות נוסף שבו נעשה שימוש בשרשראות WebAssembly בגרסה לאינטרנט.

FFMPEG.WASM היא גרסה של WebAssembly לכלי FFmpeg הפופולרי ליצירת מדיה, שמשתמש בשרשראות WebAssembly כדי לבצע קידוד יעיל של סרטונים ישירות בדפדפן.

יש עוד הרבה דוגמאות מלהיבות לשימוש בשרשראות של WebAssembly. מומלץ לצפות בהדגמות ולהביא לאינטרנט אפליקציות וספריות משלכם עם תמיכה במספר חוטים.